Migration complete
This commit is contained in:
9
_rhpz_ide_helper.php
Normal file
9
_rhpz_ide_helper.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace {
|
||||
|
||||
use App\Proxy\VisitorProxy;
|
||||
|
||||
/** @var VisitorProxy $VISITOR */
|
||||
$VISITOR = null;
|
||||
}
|
||||
@@ -5,32 +5,60 @@ namespace App\Auth;
|
||||
use Illuminate\Contracts\Auth\Guard;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/**
|
||||
* Xenforo authentification bridge.
|
||||
*/
|
||||
class XenForoGuard implements Guard
|
||||
{
|
||||
|
||||
/**
|
||||
* Authenticated user.
|
||||
* @var XenForoUser|null
|
||||
*/
|
||||
private ?XenForoUser $user = null;
|
||||
|
||||
public function __construct(private readonly Request $request) {}
|
||||
|
||||
/**
|
||||
* Check if user is logged in.
|
||||
* @return bool
|
||||
*/
|
||||
public function check(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is a guest.
|
||||
* @return bool
|
||||
*/
|
||||
public function guest(): bool
|
||||
{
|
||||
return ! $this->check();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user ID.
|
||||
* @return mixed
|
||||
*/
|
||||
public function id(): mixed
|
||||
{
|
||||
return $this->user()?->getAuthIdentifier();
|
||||
}
|
||||
|
||||
/**
|
||||
* If user is defined.
|
||||
* @return bool
|
||||
*/
|
||||
public function hasUser(): bool
|
||||
{
|
||||
return $this->user !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Login user.
|
||||
* @return XenForoUser|null
|
||||
*/
|
||||
public function user(): ?XenForoUser
|
||||
{
|
||||
if ($this->hasUser())
|
||||
@@ -64,6 +92,13 @@ class XenForoGuard implements Guard
|
||||
return $this->user = new XenForoUser($xfUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unused.
|
||||
*
|
||||
* @param array $credentials
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function validate(array $credentials = []): bool
|
||||
{
|
||||
return false;
|
||||
@@ -74,6 +109,10 @@ class XenForoGuard implements Guard
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unused.
|
||||
* @return void
|
||||
*/
|
||||
public function logout(): void
|
||||
{
|
||||
redirect('/');
|
||||
|
||||
@@ -10,11 +10,67 @@ use Filament\Panel;
|
||||
use Illuminate\Contracts\Auth\Access\Authorizable;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
|
||||
/**
|
||||
* Xenforo user custom model used for authentification.
|
||||
*
|
||||
* @property-read int $user_id
|
||||
* @property-read string $username
|
||||
* @property-read int $username_date
|
||||
* @property-read int $username_date_visible
|
||||
* @property-read string $email
|
||||
* @property-read string $custom_title
|
||||
* @property-read int $language_id
|
||||
* @property-read int $style_id
|
||||
* @property-read string $style_variation
|
||||
* @property-read string $timezone
|
||||
* @property-read int $visible
|
||||
* @property-read int $activity_visible
|
||||
* @property-read int $user_group_id
|
||||
* @property-read array $secondary_group_ids
|
||||
* @property-read int $display_style_group_id
|
||||
* @property-read int $permission_combination_id
|
||||
* @property-read int $message_count
|
||||
* @property-read int $question_solution_count
|
||||
* @property-read int $conversations_unread
|
||||
* @property-read int $register_date
|
||||
* @property-read int $last_activity
|
||||
* @property-read int $last_summary_email_date
|
||||
* @property-read int $trophy_points
|
||||
* @property-read int $alerts_unviewed
|
||||
* @property-read int $alerts_unread
|
||||
* @property-read int $avatar_date
|
||||
* @property-read int $avatar_width
|
||||
* @property-read int $avatar_height
|
||||
* @property-read int $avatar_highdpi
|
||||
* @property-read int $avatar_optimized
|
||||
* @property-read string $gravatar
|
||||
* @property-read string $user_state
|
||||
* @property-read string $security_lock
|
||||
* @property-read int $is_moderator
|
||||
* @property-read int $is_admin
|
||||
* @property-read int $is_banned
|
||||
* @property-read int $reaction_score
|
||||
* @property-read int $vote_score
|
||||
* @property-read int $warning_points
|
||||
* @property-read int $is_staff
|
||||
* @property-read string $secret_key
|
||||
* @property-read int $privacy_policy_accepted
|
||||
* @property-read int $terms_accepted
|
||||
*
|
||||
* Custom properties.
|
||||
*
|
||||
* @property-read int $rhpz_entry_count
|
||||
*/
|
||||
class XenForoUser extends XenForoData implements Authenticatable, Authorizable, FilamentUser, HasName {
|
||||
|
||||
use \Illuminate\Foundation\Auth\Access\Authorizable;
|
||||
|
||||
/**
|
||||
* Permissions identifier array.
|
||||
* @var array|null
|
||||
*/
|
||||
public ?array $permissions = null;
|
||||
|
||||
public function getAuthIdentifierName(): string
|
||||
{
|
||||
return 'user_id';
|
||||
@@ -51,9 +107,9 @@ class XenForoUser extends XenForoData implements Authenticatable, Authorizable,
|
||||
}
|
||||
|
||||
/**
|
||||
* Get XenForo avatar if it exist.
|
||||
* Get XenForo avatar if it exists.
|
||||
*
|
||||
* @param string $xfSize
|
||||
* @param string $xfSize s/m/...
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
@@ -70,6 +126,14 @@ class XenForoUser extends XenForoData implements Authenticatable, Authorizable,
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom can function. Check XF user permissions.
|
||||
*
|
||||
* @param string $permissionGroup
|
||||
* @param string $permissionName
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function _can(string $permissionGroup, string $permissionName): bool
|
||||
{
|
||||
if( !$this->permissions ){
|
||||
@@ -105,4 +169,9 @@ class XenForoUser extends XenForoData implements Authenticatable, Authorizable,
|
||||
{
|
||||
return 'user_id';
|
||||
}
|
||||
|
||||
public function validState(): bool
|
||||
{
|
||||
return $this->user_state === 'valid';
|
||||
}
|
||||
}
|
||||
|
||||
40
app/Console/Commands/FixEncodedSlugs.php
Normal file
40
app/Console/Commands/FixEncodedSlugs.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Attributes\Signature;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
#[Signature('fix:encoded-slugs')]
|
||||
class FixEncodedSlugs extends Command
|
||||
{
|
||||
private const array TABLES = [
|
||||
'entries', 'games', 'platforms', 'genres', 'languages', 'modifications',
|
||||
'levels', 'statuses', 'authors', 'categories', 'systems',
|
||||
];
|
||||
|
||||
public function handle()
|
||||
{
|
||||
foreach (self::TABLES as $table) {
|
||||
$rows = DB::table($table)->where('slug', 'like', '%\\%%')->get(['id', 'slug']);
|
||||
$fixed = 0;
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$decoded = rawurldecode($row->slug);
|
||||
if ($decoded === $row->slug) continue;
|
||||
|
||||
try {
|
||||
DB::table($table)->where('id', $row->id)->update(['slug' => $decoded]);
|
||||
$fixed++;
|
||||
} catch (\Throwable $e) {
|
||||
$this->warn("{$table}#{$row->id} : collision '{$decoded}', ignored ({$e->getMessage()}).");
|
||||
}
|
||||
}
|
||||
|
||||
if ($fixed > 0) {
|
||||
$this->info("{$table}: {$fixed} fixed slugs.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
47
app/Console/Commands/FixEntriesDescription.php
Normal file
47
app/Console/Commands/FixEntriesDescription.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Helpers\MigrationHelpers;
|
||||
use Illuminate\Console\Attributes\Signature;
|
||||
use Illuminate\Console\Attributes\Description;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use League\HTMLToMarkdown\HtmlConverter;
|
||||
|
||||
#[Signature('fix:entries-description')]
|
||||
#[Description('Reconvert HTML descriptions to Markdown')]
|
||||
class FixEntriesDescription extends Command
|
||||
{
|
||||
public function handle()
|
||||
{
|
||||
$converter = new HtmlConverter(['hard_break' => true]);
|
||||
|
||||
$rows = DB::table('migrations_logs')
|
||||
->where('source_system', 'wp')
|
||||
->where('source_table', 'wp_posts')
|
||||
->where('target_table', 'entries')
|
||||
->get(['source_id', 'target_id']);
|
||||
|
||||
$this->info("{$rows->count()} entries touched.");
|
||||
|
||||
$this->withProgressBar($rows, function ($row) use ($converter) {
|
||||
$rawHtml = DB::connection('old_wp')->table('posts')
|
||||
->where('ID', $row->source_id)
|
||||
->value('post_content');
|
||||
|
||||
if ($rawHtml === null) return;
|
||||
|
||||
$markdown = trim($rawHtml) === '' ? $rawHtml : $converter->convert(MigrationHelpers::wpAutoP($rawHtml));
|
||||
|
||||
DB::table('entries')->where('id', $row->target_id)->update([
|
||||
'description' => $markdown,
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
});
|
||||
|
||||
$this->newLine();
|
||||
$this->info('Process finished.');
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
44
app/Console/Commands/MigrateCategoriesConfigure.php
Normal file
44
app/Console/Commands/MigrateCategoriesConfigure.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Attributes\Description;
|
||||
use Illuminate\Console\Attributes\Signature;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
#[Signature('migrate:categories:configure')]
|
||||
#[Description('Configure categories table.')]
|
||||
class MigrateCategoriesConfigure extends Command
|
||||
{
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$taxonomyMap = [];
|
||||
$taxonomy = "a";
|
||||
$section = "";
|
||||
|
||||
while( $taxonomy !== "" ){
|
||||
$taxonomy = "";
|
||||
$section = "";
|
||||
|
||||
$taxonomy = $this->ask("Write WP taxonomy name. Write nothing if you want to save changes.", "" );
|
||||
if( $taxonomy == "" )
|
||||
break;
|
||||
|
||||
$section = $this->ask("Write entry section equivalent.");
|
||||
|
||||
$taxonomyMap[$taxonomy] = $section;
|
||||
}
|
||||
|
||||
DB::table('migration_settings')
|
||||
->updateOrInsert([
|
||||
'key' => 'wp_categories_to_entry_sections'
|
||||
],[
|
||||
'value' => json_encode($taxonomyMap), 'updated_at' => now()
|
||||
]);
|
||||
|
||||
$this->info('WP Categories to entry sections has been configured.');
|
||||
}
|
||||
}
|
||||
92
app/Console/Commands/MigrateCategoriesExecute.php
Normal file
92
app/Console/Commands/MigrateCategoriesExecute.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Attributes\Description;
|
||||
use Illuminate\Console\Attributes\Signature;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
#[Signature('migrate:categories:execute')]
|
||||
#[Description('Migrate categories, make sure you have configured the migration before that.')]
|
||||
class MigrateCategoriesExecute extends Command
|
||||
{
|
||||
|
||||
private const string TABLE = 'categories';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$taxMap = json_decode(DB::table('migration_settings')->where('key', 'wp_categories_to_entry_sections')->value('value'), true);
|
||||
|
||||
if( !$taxMap ){
|
||||
$this->error("No WP taxonomies need to be transferred.");
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
if( $this->ask("Are you sure you want launch that migration? Write ok if you want to launch it.", "") !== 'ok' ){
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$table = self::TABLE;
|
||||
|
||||
foreach ( $taxMap as $wpTax => $restrictedTo )
|
||||
{
|
||||
$this->info("Migrate: {$wpTax} restricted to {$table}");
|
||||
|
||||
$terms = DB::connection('old_wp')
|
||||
->table('term_taxonomy')
|
||||
->join('terms', 'term_taxonomy.term_id', '=', 'terms.term_id')
|
||||
->where('term_taxonomy.taxonomy', $wpTax)
|
||||
->select('term_taxonomy.term_taxonomy_id', 'terms.name', 'terms.slug')
|
||||
->get();
|
||||
|
||||
$this->withProgressBar($terms, function ($term) use ($table, $restrictedTo) {
|
||||
|
||||
$exists = DB::table('migrations_logs')
|
||||
->where('source_system', 'wp')
|
||||
->where('source_table', 'wp_term_taxonomy')
|
||||
->where('source_id', $term->term_taxonomy_id )
|
||||
->exists();
|
||||
|
||||
if( $exists )
|
||||
return;
|
||||
|
||||
$existing = DB::table( $table )->where('slug', $term->slug)->first();
|
||||
|
||||
if( $existing === null) {
|
||||
$Id = DB::table($table)
|
||||
->insertGetId([
|
||||
'name' => $term->name, 'slug' => $term->slug,
|
||||
'restricted_to' => json_encode([$restrictedTo]),
|
||||
'created_at' => now(), 'updated_at' => now()
|
||||
]);
|
||||
} else {
|
||||
$Id = $existing->id;
|
||||
$restrictedToField = json_decode($existing->restricted_to, true) ?? [];
|
||||
if( !in_array( $restrictedTo, $restrictedToField, true ) ){
|
||||
$restrictedToField[] = $restrictedTo;
|
||||
DB::table($table)
|
||||
->where('id', $Id)
|
||||
->update(['restricted_to' => json_encode($restrictedToField), 'updated_at' => now()]);
|
||||
}
|
||||
}
|
||||
|
||||
DB::table('migrations_logs')->insert([
|
||||
'source_system' => 'wp',
|
||||
'source_table' => 'wp_term_taxonomy',
|
||||
'source_id' => $term->term_taxonomy_id,
|
||||
'target_table' => $table,
|
||||
'target_id' => $Id,
|
||||
'status' => 'done',
|
||||
'migrated_at' => now(),
|
||||
'created_at' => now(),
|
||||
'updated_at' => now()
|
||||
]);
|
||||
});
|
||||
$this->newLine();
|
||||
}
|
||||
|
||||
$this->info("Migration done");
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
80
app/Console/Commands/MigrateEntriesComments.php
Normal file
80
app/Console/Commands/MigrateEntriesComments.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Attributes\Signature;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Database\Connection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
#[Signature('migrate:entries:comments {--import-log-table=} {--limit=}')]
|
||||
class MigrateEntriesComments extends Command
|
||||
{
|
||||
public function handle()
|
||||
{
|
||||
$logTable = $this->option('import-log-table');
|
||||
if( !$logTable ){
|
||||
$this->error( 'XenForo import log table required' );
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$this->commentsThread( 'entries', 'wp_posts', $logTable );
|
||||
$this->commentsThread( 'news', 'wp_posts__news', $logTable );
|
||||
|
||||
$this->info( "Done" );
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function commentsThread( string $table, string $sourceTable, string $logTable ): void
|
||||
{
|
||||
$query = DB::table('migrations_logs')
|
||||
->where('source_system', 'wp' )
|
||||
->where('source_table', $sourceTable )
|
||||
->where('target_table', $table );
|
||||
|
||||
if( $limit = $this->option('limit') ){
|
||||
$query->limit((int)$limit);
|
||||
}
|
||||
|
||||
$rows = $query->get(['source_id', 'target_id']);
|
||||
$this->info( "{$rows->count()} need migration logs" );
|
||||
|
||||
$stats = [ 'updated' => 0, 'no_meta' => 0, 'no_new_thread' => 0 ];
|
||||
|
||||
$this->withProgressBar( $rows, function ( $row ) use( $table, $logTable, &$stats ) {
|
||||
|
||||
$oldThreadId = DB::connection('old_wp')
|
||||
->table('postmeta')
|
||||
->where('post_id', $row->source_id )
|
||||
->where('meta_key', 'xf_thread_id')
|
||||
->value('meta_value');
|
||||
|
||||
if( !$oldThreadId ){
|
||||
$stats['no_meta']++;
|
||||
return;
|
||||
}
|
||||
|
||||
$newThreadId = DB::connection('xenforo')
|
||||
->withoutTablePrefix( function( Connection $db ) use( $logTable, $oldThreadId ){
|
||||
return $db->table( $logTable )
|
||||
->where('content_type', 'thread')
|
||||
->where('old_id', (string) $oldThreadId )
|
||||
->value('new_id');
|
||||
});
|
||||
|
||||
if( !$newThreadId ){
|
||||
$stats['no_new_thread']++;
|
||||
return;
|
||||
}
|
||||
|
||||
DB::table( $table )->where('id', $row->target_id )->update([
|
||||
'comments_thread_id' => (int) $newThreadId
|
||||
]);
|
||||
|
||||
$stats['updated']++;
|
||||
});
|
||||
|
||||
$this->newLine();
|
||||
$this->info( "Updated: {$stats['updated']}, No new thread: {$stats['no_new_thread']}, No meta: {$stats['no_meta']}" );
|
||||
}
|
||||
}
|
||||
295
app/Console/Commands/MigrateEntriesExecute.php
Normal file
295
app/Console/Commands/MigrateEntriesExecute.php
Normal file
@@ -0,0 +1,295 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Helpers\MigrationHelpers;
|
||||
use Illuminate\Console\Attributes\Description;
|
||||
use Illuminate\Console\Attributes\Signature;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
#[Signature('migrate:entries:execute {--limit=}')]
|
||||
#[Description('Migrate WP entries')]
|
||||
class MigrateEntriesExecute extends Command
|
||||
{
|
||||
private const array WP_CPTS = ['translations', 'romhacks', 'homebrew', 'utilities', 'documents' ];
|
||||
|
||||
private const array STATE_MAP = [
|
||||
'draft' => 'draft',
|
||||
'pending' => 'pending',
|
||||
'publish' => 'published',
|
||||
'private' => 'hidden',
|
||||
'locked' => 'locked',
|
||||
];
|
||||
|
||||
private const array ACF_FIELDS = [
|
||||
'entry_title', 'version_number', 'release_date', 'release_site',
|
||||
'youtube_video', 'staff', 'hashes',
|
||||
];
|
||||
|
||||
private const array MULTI_TAXONOMIES = [
|
||||
'language' => 'languages',
|
||||
'modifications' => 'modifications',
|
||||
'author-name' => 'authors',
|
||||
'document-category' => 'categories',
|
||||
'utility-category' => 'categories',
|
||||
'utility-os' => 'systems',
|
||||
];
|
||||
|
||||
private array $stats = [];
|
||||
|
||||
private function getSingle( ?int $term_id, string $targetTable ): ?int
|
||||
{
|
||||
if (!$term_id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return DB::table('migrations_logs')
|
||||
->where('source_system', 'wp')
|
||||
->where('target_table', $targetTable )
|
||||
->where('source_id', $term_id)
|
||||
->value('target_id');
|
||||
}
|
||||
|
||||
private function uniqueSlug(string $baseSlug, string $table, ?int $ignoreId = null): string
|
||||
{
|
||||
$slug = $baseSlug;
|
||||
$i = 1;
|
||||
while (
|
||||
DB::table($table)->where('slug', $slug)
|
||||
->when($ignoreId, fn ($q) => $q->where('id', '!=', $ignoreId))
|
||||
->exists() && $i < 100
|
||||
) {
|
||||
$slug = $baseSlug . '-' . $i++;
|
||||
}
|
||||
if ($i >= 100) {
|
||||
$slug = (string) \Str::uuid();
|
||||
}
|
||||
return $slug;
|
||||
}
|
||||
|
||||
private function parseStaffCredits(?string $raw): array
|
||||
{
|
||||
if (!$raw || trim($raw) === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$credits = [];
|
||||
foreach (preg_split('/\r\n|\r|\n/', $raw) as $line) {
|
||||
$line = trim($line);
|
||||
if ($line === '') continue;
|
||||
|
||||
if (preg_match('/^(.+?):\s*(.*)$/', $line, $m)) {
|
||||
$credits[] = ['name' => trim($m[1]), 'description' => trim($m[2])];
|
||||
} elseif (!empty($credits)) {
|
||||
$last = array_key_last($credits);
|
||||
$credits[$last]['description'] = trim($credits[$last]['description'] . ' ' . $line);
|
||||
} else {
|
||||
$credits[] = ['name' => $line, 'description' => ''];
|
||||
}
|
||||
}
|
||||
|
||||
return $credits;
|
||||
}
|
||||
|
||||
private function parseHashes(?string $raw): array
|
||||
{
|
||||
if (!$raw || trim($raw) === '') return [];
|
||||
|
||||
$results = [];
|
||||
foreach (preg_split('/\n\s*\n/', trim($raw)) as $block) {
|
||||
$fields = [];
|
||||
foreach (preg_split('/\r\n|\r|\n/', trim($block)) as $line) {
|
||||
$line = trim($line);
|
||||
if ($line === '' || !str_contains($line, ':')) continue;
|
||||
[$key, $value] = array_map('trim', explode(':', $line, 2));
|
||||
$fields[strtolower(preg_replace('/[^a-z0-9]/i', '', $key))] = $value;
|
||||
}
|
||||
if (empty($fields)) continue;
|
||||
|
||||
$results[] = [
|
||||
'filename' => $fields['filename'] ?? '',
|
||||
'hash_crc32' => $fields['crc32'] ?? '',
|
||||
'hash_sha1' => $fields['sha1'] ?? '',
|
||||
];
|
||||
}
|
||||
return $results;
|
||||
}
|
||||
|
||||
private function attachMany(int $entryId, string $pivotTable, string $foreignKey, array $ttids, string $targetTable): void
|
||||
{
|
||||
if (empty($ttids)) return;
|
||||
|
||||
$ids = DB::table('migrations_logs')
|
||||
->where('source_system', 'wp')->where('target_table', $targetTable)
|
||||
->whereIn('source_id', $ttids)->pluck('target_id');
|
||||
|
||||
foreach ($ids as $id) {
|
||||
DB::table($pivotTable)->insertOrIgnore(['entry_id' => $entryId, $foreignKey => $id]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \StdClass $post
|
||||
* @param string $cpt
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function migratePost( $post, string $cpt ): void
|
||||
{
|
||||
$exists = DB::table('migrations_logs')
|
||||
->where('source_system', 'wp')
|
||||
->where('source_table', 'wp_posts')
|
||||
->where('source_id', $post->ID)
|
||||
->exists();
|
||||
|
||||
if ($exists)
|
||||
return;
|
||||
|
||||
$meta = DB::connection('old_wp')
|
||||
->table('postmeta')
|
||||
->where('post_id', $post->ID)
|
||||
->whereIn('meta_key', self::ACF_FIELDS)
|
||||
->pluck('meta_value', 'meta_key');
|
||||
|
||||
$terms = DB::connection('old_wp')
|
||||
->table('term_relationships as tr')
|
||||
->join('term_taxonomy as tt', 'tr.term_taxonomy_id', '=', 'tt.term_taxonomy_id')
|
||||
->where('tr.object_id', $post->ID)
|
||||
->whereIn('tt.taxonomy', array_merge(['game', 'platform', 'hack-status', 'experience-level'], array_keys(self::MULTI_TAXONOMIES)) )
|
||||
->select('tt.taxonomy', 'tt.term_taxonomy_id')
|
||||
->get();
|
||||
|
||||
$byTax = [];
|
||||
foreach ($terms as $term) {
|
||||
$byTax[$term->taxonomy][] = $term->term_taxonomy_id;
|
||||
}
|
||||
|
||||
$gameId = null;
|
||||
if( !empty( $byTax['game'][0] ) && !empty( $byTax['platform'][0] ) )
|
||||
{
|
||||
$gameId = DB::table('migration_game_plan')
|
||||
->where('wp_game_id', $byTax['game'][0])
|
||||
->where('wp_platform_id', $byTax['platform'][0])
|
||||
->value('game_id');
|
||||
|
||||
if( !$gameId )
|
||||
$this->stats['missing_game_plan']++;
|
||||
}
|
||||
|
||||
$statusId = $this->getSingle( $byTax['hack-status'][0] ?? null, 'statuses' );
|
||||
$levelId = $this->getSingle( $byTax['experience-level'][0] ?? null, 'levels' );
|
||||
|
||||
$userId = DB::table('migration_user_plan')
|
||||
->where('wp_user_id', $post->post_author )
|
||||
->value('user_id');
|
||||
|
||||
if( !$userId ) {
|
||||
$this->stats['missing_author']++;
|
||||
return;
|
||||
}
|
||||
|
||||
$title = $meta['entry_title'] ?? null;
|
||||
if( $cpt === 'translations' && !$title && $gameId )
|
||||
{
|
||||
$title = DB::table('games')->where('id', $gameId)->value('name');
|
||||
}
|
||||
|
||||
$slug = $this->uniqueSlug( rawurldecode($post->post_name), 'entries' );
|
||||
|
||||
$entryId = DB::table('entries')->insertGetId([
|
||||
'type' => $cpt,
|
||||
'title' => $title,
|
||||
'complete_title' => $post->post_title ?? null,
|
||||
'slug' => $slug,
|
||||
'description' => MigrationHelpers::htmlToMarkdown($post->post_content),
|
||||
'state' => self::STATE_MAP[$post->post_status],
|
||||
'game_id' => $gameId,
|
||||
'platform_id' => null,
|
||||
'status_id' => $statusId,
|
||||
'level_id' => $levelId,
|
||||
'version' => $meta['version_number'] ?? null,
|
||||
'release_date' => $meta['release_date'] ?? null,
|
||||
'staff_credits' => json_encode( $this->parseStaffCredits($meta['staff'] ?? null)),
|
||||
'relevant_link' => $meta['release_site'] ?? null,
|
||||
'youtube_link' => $meta['youtube_video'] ?? null,
|
||||
'user_id' => $userId,
|
||||
'created_at' => $post->post_date,
|
||||
'updated_at' => $post->post_modified,
|
||||
]);
|
||||
|
||||
$this->attachMany($entryId, 'entry_authors', 'author_id', $byTax['author-name'] ?? [], 'authors');
|
||||
$this->attachMany($entryId, 'entry_languages', 'language_id', $byTax['language'] ?? [], 'languages');
|
||||
|
||||
if( $cpt === 'romhacks')
|
||||
$this->attachMany($entryId, 'entry_modifications', 'modification_id', $byTax['modifications'] ?? [], 'modifications');
|
||||
|
||||
if ($cpt === 'utilities') {
|
||||
$this->attachMany($entryId, 'entry_categories', 'category_id', $byTax['utility-category'] ?? [], 'categories');
|
||||
$this->attachMany($entryId, 'entry_systems', 'system_id', $byTax['utility-os'] ?? [], 'systems');
|
||||
}
|
||||
if ($cpt === 'documents') {
|
||||
$this->attachMany($entryId, 'entry_categories', 'category_id', $byTax['document-category'] ?? [], 'categories');
|
||||
}
|
||||
|
||||
if( $cpt === 'translations' || $cpt === 'romhacks' ){
|
||||
foreach ( $this->parseHashes( $meta['hashes'] ?? null ) as $hash ) {
|
||||
DB::table('entry_hashes')->insert([
|
||||
'entry_id' => $entryId,
|
||||
'filename' => $hash['filename'],
|
||||
'hash_crc32' => $hash['hash_crc32'],
|
||||
'hash_sha1' => $hash['hash_sha1'],
|
||||
'verified' => 'TBD',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
DB::table('migrations_logs')->insert([
|
||||
'source_system' => 'wp',
|
||||
'source_table' => 'wp_posts',
|
||||
'source_id' => $post->ID,
|
||||
'target_table' => 'entries',
|
||||
'target_id' => $entryId,
|
||||
'status' => 'done',
|
||||
'migrated_at' => now(),
|
||||
'updated_at' => now(),
|
||||
'created_at' => now(),
|
||||
]);
|
||||
|
||||
$this->stats['created']++;
|
||||
|
||||
}
|
||||
|
||||
private function migrateCpt( string $cpt ): void
|
||||
{
|
||||
$this->info( "Current migration of : $cpt ");
|
||||
$this->stats = [ 'missing_author' => 0, 'missing_game_plan' => 0, 'created' => 0 ];
|
||||
|
||||
$query = DB::connection('old_wp')
|
||||
->table('posts')
|
||||
->where('post_type', $cpt)
|
||||
->whereIn('post_status', array_keys(self::STATE_MAP))
|
||||
;
|
||||
|
||||
if( $limit = $this->option('limit') ) {
|
||||
$query->limit((int) $limit);
|
||||
}
|
||||
|
||||
$posts = $query->select('ID', 'post_title', 'post_name', 'post_content', 'post_status', 'post_author', 'post_date', 'post_modified')
|
||||
->get();
|
||||
|
||||
$this->withProgressBar($posts, fn( $post ) => $this->migratePost( $post, $cpt ) );
|
||||
|
||||
$this->newLine();
|
||||
$this->info( "Created {$this->stats['created']} entries'. No authors {$this->stats['missing_author']}. Missing game {$this->stats['missing_game_plan']} entries" );
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
foreach ( self::WP_CPTS as $cpt ) {
|
||||
$this->migrateCpt( $cpt );
|
||||
}
|
||||
}
|
||||
}
|
||||
146
app/Console/Commands/MigrateEntriesImages.php
Normal file
146
app/Console/Commands/MigrateEntriesImages.php
Normal file
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Attributes\Description;
|
||||
use Illuminate\Console\Attributes\Signature;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
#[Signature('migrate:entries:images {--wp-uploads-path=} {--limit=}')]
|
||||
#[Description("Migrate WP Main images and galeries for entries.")]
|
||||
class MigrateEntriesImages extends Command
|
||||
{
|
||||
private const string GALLERYABLE_TYPE = 'App\\Models\\Entry';
|
||||
|
||||
private function copyAttachment( int $attachmentId, string $wpUploadsPath, string $destinationPath ): ?string
|
||||
{
|
||||
$relativePath = DB::connection('old_wp')
|
||||
->table('postmeta')
|
||||
->where('post_id', $attachmentId)
|
||||
->where('meta_key', '_wp_attached_file')
|
||||
->value('meta_value');
|
||||
|
||||
if( !$relativePath )
|
||||
return null;
|
||||
|
||||
$sourcePath = rtrim($wpUploadsPath, '/') . '/' . $relativePath;
|
||||
if( !is_file($sourcePath) )
|
||||
return null;
|
||||
|
||||
$extension = pathinfo($sourcePath, PATHINFO_EXTENSION);
|
||||
$filename = Str::random(40) . ($extension ? '.' . $extension : '');
|
||||
$destinationRelative = $destinationPath . '/' . $filename;
|
||||
|
||||
$stream = fopen($sourcePath, 'r');
|
||||
Storage::disk('public')->put($destinationRelative, $stream);
|
||||
if( is_resource($stream) )
|
||||
fclose($stream);
|
||||
|
||||
return $destinationRelative;
|
||||
}
|
||||
|
||||
private function processEntry( int $wpPostId, int $entryId, string $wpUploadsPath, array &$stats ): void
|
||||
{
|
||||
$exists = DB::table('migrations_logs')
|
||||
->where('source_system', 'wp')
|
||||
->where('source_table', 'wp_posts__attachments')
|
||||
->where('source_id', $wpPostId)
|
||||
->exists();
|
||||
|
||||
if( $exists )
|
||||
return;
|
||||
|
||||
$thumbnailId = DB::connection('old_wp')
|
||||
->table('postmeta')
|
||||
->where('post_id', $wpPostId)
|
||||
->where('meta_key', '_thumbnail_id')
|
||||
->value('meta_value');
|
||||
|
||||
if( $thumbnailId ){
|
||||
$newPath = $this->copyAttachment( (int) $thumbnailId, $wpUploadsPath, 'entries/main-images' );
|
||||
if( $newPath ){
|
||||
DB::table('entries')
|
||||
->where('id', $entryId)
|
||||
->update(['main_image' => $newPath]);
|
||||
$stats['main_image']++;
|
||||
} else {
|
||||
$stats['missing_files']++;
|
||||
}
|
||||
}
|
||||
|
||||
$galleryRaw = DB::connection('old_wp')
|
||||
->table('postmeta')
|
||||
->where('post_id', $wpPostId)
|
||||
->where('meta_key', 'my_gallery')
|
||||
->value('meta_value');
|
||||
|
||||
$attachmentIds = $galleryRaw ? (@unserialize($galleryRaw) ?: []) : [];
|
||||
|
||||
foreach ( array_values( $attachmentIds ) as $order => $attachmentId ) {
|
||||
$newPath = $this->copyAttachment( (int) $attachmentId, $wpUploadsPath, "entries/gallery-images/{$entryId}" );
|
||||
if( !$newPath ){
|
||||
$stats['missing_files']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
DB::table('galleries')
|
||||
->insert([
|
||||
'galleryable_type' => self::GALLERYABLE_TYPE,
|
||||
'galleryable_id' => $entryId,
|
||||
'image' => $newPath,
|
||||
'order' => $order,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
$stats['gallery_images']++;
|
||||
}
|
||||
|
||||
DB::table('migrations_logs')
|
||||
->insert([
|
||||
'source_system' => 'wp',
|
||||
'source_table' => 'wp_posts__attachments',
|
||||
'source_id' => $wpPostId,
|
||||
'target_table' => 'entries',
|
||||
'target_id' => $entryId,
|
||||
'status' => 'done',
|
||||
'migrated_at' => now(),
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$wpUploadsPath = $this->option('wp-uploads-path');
|
||||
if( !$wpUploadsPath || !is_dir($wpUploadsPath) ){
|
||||
$this->error('Missing WP Uploads Path');
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$query = DB::table('migrations_logs')
|
||||
->where('source_system', 'wp')
|
||||
->where('source_table', 'wp_posts')
|
||||
->where('target_table', 'entries')
|
||||
;
|
||||
|
||||
if( $limit = $this->option('limit') ){
|
||||
$query->limit((int)$limit);
|
||||
}
|
||||
|
||||
$rows = $query->get(['source_id', 'target_id']);
|
||||
$this->info("{$rows->count()} entries need to be migrated");
|
||||
|
||||
$stats = ['main_image' => 0, 'gallery_images' => 0, 'missing_files' => 0 ];
|
||||
|
||||
$this->withProgressBar($rows, function($row) use($wpUploadsPath, &$stats) {
|
||||
$this->processEntry( $row->source_id, $row->target_id, $wpUploadsPath, $stats );
|
||||
});
|
||||
|
||||
$this->newLine();
|
||||
$this->info("Migrated attachments. Main images: {$stats['main_image']}, Galleries: {$stats['gallery_images']}, Missing files: {$stats['missing_files']}");
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
169
app/Console/Commands/MigrateGamesExecute.php
Normal file
169
app/Console/Commands/MigrateGamesExecute.php
Normal file
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Platform;
|
||||
use Illuminate\Console\Attributes\Description;
|
||||
use Illuminate\Console\Attributes\Signature;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
#[Signature('migrate:games:execute')]
|
||||
#[Description("Migrate all games from WP posts.")]
|
||||
class MigrateGamesExecute extends Command
|
||||
{
|
||||
private const array WP_CPTS = [ 'translations', 'romhacks', 'homebrew', 'utilities', 'documents', 'lua-scripts' ];
|
||||
private const array RELATED_TAXS = ['game', 'platform', 'genre'];
|
||||
private const int DEFAULT_GENRE_ID = 1;
|
||||
private const int NO_GENRE_SENTINEL = 0;
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$rows = DB::connection('old_wp')
|
||||
->table('term_relationships as tr')
|
||||
->join('term_taxonomy as tt', 'tr.term_taxonomy_id', '=', 'tt.term_taxonomy_id')
|
||||
->join('posts as p', 'tr.object_id', '=', 'p.ID')
|
||||
->whereIn('tt.taxonomy', self::RELATED_TAXS )
|
||||
->whereIn('p.post_type', self::WP_CPTS )
|
||||
->where('p.post_status', '!=', 'trash')
|
||||
->select('p.ID as post_id', 'tt.taxonomy', 'tt.term_taxonomy_id' )
|
||||
->get();
|
||||
|
||||
$byPost = [];
|
||||
$ignored = 0;
|
||||
foreach( $rows as $row ){
|
||||
if( isset( $byPost[ $row->post_id ][$row->taxonomy] ) ){
|
||||
$ignored++;
|
||||
continue;
|
||||
}
|
||||
$byPost[ $row->post_id ][$row->taxonomy] = $row->term_taxonomy_id;
|
||||
}
|
||||
if( $ignored ){
|
||||
$this->warn("$ignored posts with multiple taxs will be ignored.");
|
||||
}
|
||||
|
||||
$structure = [];
|
||||
$ignoredNoPlatform = 0;
|
||||
|
||||
foreach ( $byPost as $data )
|
||||
{
|
||||
if( empty( $data['game'] ) )
|
||||
continue;
|
||||
|
||||
if( empty( $data['platform'] ) ){
|
||||
$ignoredNoPlatform++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$gameId = $data['game'];
|
||||
$platformId = $data['platform'];
|
||||
$genreId = $data['genre'] ?? self::NO_GENRE_SENTINEL;
|
||||
|
||||
$structure[$gameId][$platformId]['count'] = ( $structure[$gameId][$platformId]['count'] ?? 0 ) + 1;
|
||||
$structure[$gameId][$platformId]['genres'][$genreId] = ( $structure[$gameId][$platformId]['genres'][$genreId] ?? 0 ) + 1;
|
||||
}
|
||||
if( $ignoredNoPlatform ){
|
||||
$this->warn("$ignoredNoPlatform posts with no platforms will be ignored.");
|
||||
}
|
||||
|
||||
$games = DB::connection('old_wp')
|
||||
->table('term_taxonomy')
|
||||
->join('terms', 'term_taxonomy.term_id', '=', 'terms.term_id')
|
||||
->where('term_taxonomy.taxonomy', 'game' )
|
||||
->select('term_taxonomy.term_taxonomy_id', 'terms.name', 'terms.slug' )
|
||||
->get()
|
||||
->keyBy('term_taxonomy_id');
|
||||
|
||||
$platformMap = DB::table('migrations_logs')
|
||||
->where('source_system', 'wp' )
|
||||
->where('target_table', 'platforms')
|
||||
->pluck('target_id', 'source_id');
|
||||
|
||||
$genreMap = DB::table('migrations_logs')
|
||||
->where('source_system', 'wp' )
|
||||
->where('target_table', 'genres')
|
||||
->pluck('target_id', 'source_id');
|
||||
|
||||
if( $this->ask("Are you sure you want launch that migration? Write ok if you want to launch it.", "") !== 'ok' ){
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$created = 0;
|
||||
$genreConflicts = 0;
|
||||
|
||||
foreach( $structure as $gameId => $platforms )
|
||||
{
|
||||
$game = $games->get( $gameId );
|
||||
if( !$game )
|
||||
continue;
|
||||
|
||||
foreach( $platforms as $platformId => $info )
|
||||
{
|
||||
$alreadyExists = DB::table('migration_game_plan')
|
||||
->where('wp_game_id', $gameId )
|
||||
->where('wp_platform_id', $platformId )
|
||||
->exists();
|
||||
if( $alreadyExists )
|
||||
continue;
|
||||
|
||||
$newPlatformId = $platformMap[$platformId] ?? null;
|
||||
if( !$newPlatformId ){
|
||||
$this->warn("$gameId ignored because platform $platformId does not exist in Laravel.");
|
||||
continue;
|
||||
}
|
||||
|
||||
$genres = $info['genres'];
|
||||
$newGenreId = null;
|
||||
$topId = null;
|
||||
$genreConflict = false;
|
||||
|
||||
arsort( $genres );
|
||||
$topId = array_key_first( $genres );
|
||||
$topCount = $genres[$topId];
|
||||
$tied = count(array_filter(
|
||||
$genres, fn( $c ) => $c === $topCount
|
||||
));
|
||||
if( $tied > 1 ){
|
||||
$genreConflict = true;
|
||||
$genreConflicts++;
|
||||
}
|
||||
$newGenreId = $topId === self::NO_GENRE_SENTINEL
|
||||
? self::DEFAULT_GENRE_ID
|
||||
: ($genreMap[$topId] ?? self::DEFAULT_GENRE_ID);
|
||||
|
||||
$gameSlug = $game->slug;
|
||||
if (count($platforms) > 1) {
|
||||
$platformSlug = DB::table('platforms')->where('id', $newPlatformId)->value('slug');
|
||||
$gameSlug = $game->slug . '-' . $platformSlug;
|
||||
}
|
||||
|
||||
$newGameId = DB::table('games')
|
||||
->insertGetId([
|
||||
'name' => $game->name,
|
||||
'slug' => $gameSlug,
|
||||
'platform_id' => $newPlatformId,
|
||||
'genre_id' => $newGenreId,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
DB::table('migration_game_plan')->insert([
|
||||
'wp_game_id' => $gameId,
|
||||
'wp_platform_id' => $platformId,
|
||||
'game_id' => $newGameId,
|
||||
'wp_genre_id' => $topId === self::NO_GENRE_SENTINEL ? null : $topId,
|
||||
'post_count' => $info['count'],
|
||||
'genre_conflict' => $genreConflict,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$created++;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
$this->info("$created games created. $genreConflicts genre conflicts.");
|
||||
}
|
||||
}
|
||||
149
app/Console/Commands/MigrateNewsExecute.php
Normal file
149
app/Console/Commands/MigrateNewsExecute.php
Normal file
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Helpers\MigrationHelpers;
|
||||
use Illuminate\Console\Attributes\Description;
|
||||
use Illuminate\Console\Attributes\Signature;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
#[Signature('migrate:news:execute {--limit=}')]
|
||||
#[Description("Migrate WP news to news table")]
|
||||
class MigrateNewsExecute extends Command
|
||||
{
|
||||
private const array STATE_MAP = [
|
||||
'draft' => 'draft',
|
||||
'pending' => 'pending',
|
||||
'publish' => 'published',
|
||||
'private' => 'hidden',
|
||||
'locked' => 'locked',
|
||||
];
|
||||
|
||||
private array $stats = [];
|
||||
|
||||
private function uniqueSlug(string $baseSlug, string $table, ?int $ignoreId = null): string
|
||||
{
|
||||
$slug = $baseSlug;
|
||||
$i = 1;
|
||||
while (
|
||||
DB::table($table)->where('slug', $slug)
|
||||
->when($ignoreId, fn ($q) => $q->where('id', '!=', $ignoreId))
|
||||
->exists() && $i < 100
|
||||
) {
|
||||
$slug = $baseSlug . '-' . $i++;
|
||||
}
|
||||
if ($i >= 100) {
|
||||
$slug = (string) Str::uuid();
|
||||
}
|
||||
return $slug;
|
||||
}
|
||||
|
||||
private function migrateNews($post): void
|
||||
{
|
||||
$exists = DB::table('migrations_logs')
|
||||
->where('source_system', 'wp')
|
||||
->where('source_table', 'wp_posts__news')
|
||||
->where('source_id', $post->ID)
|
||||
->exists();
|
||||
if ($exists)
|
||||
return;
|
||||
|
||||
$meta = DB::connection('old_wp')
|
||||
->table('postmeta')
|
||||
->where('post_id', $post->ID)
|
||||
->whereIn('meta_key', ['release_site', 'romhacks_page', 'youtube_video' ])
|
||||
->pluck('meta_value', 'meta_key');
|
||||
|
||||
$categoryTtId = DB::connection('old_wp')
|
||||
->table('term_relationships as tr')
|
||||
->join('term_taxonomy as tt', 'tr.term_taxonomy_id', '=', 'tt.term_taxonomy_id')
|
||||
->where('tr.object_id', $post->ID)
|
||||
->where('tt.taxonomy', 'news-category')
|
||||
->value('tt.term_taxonomy_id');
|
||||
|
||||
$categoryId = null;
|
||||
if( $categoryTtId ){
|
||||
$categoryId = DB::table('migrations_logs')
|
||||
->where('source_system', 'wp')
|
||||
->where('target_table', 'categories' )
|
||||
->where('source_id', $categoryTtId)
|
||||
->value('target_id');
|
||||
if( !$categoryId ){
|
||||
$this->stats['missing_category']++;
|
||||
}
|
||||
}
|
||||
|
||||
$userId = DB::table('migration_user_plan')
|
||||
->where('wp_user_id', $post->post_author )
|
||||
->value('user_id');
|
||||
|
||||
if( !$userId ){
|
||||
$this->stats['missing_author']++;
|
||||
return;
|
||||
}
|
||||
|
||||
$slug = $this->uniqueSlug(rawurldecode($post->post_name), 'news');
|
||||
$description = trim($post->post_content) === '' ? '' : MigrationHelpers::htmlToMarkdown($post->post_content);
|
||||
|
||||
if( isset( $meta['romhacks_page'] ) && $meta['romhacks_page'] !== null && $meta['romhacks_page'] !== '' ){
|
||||
$description .= "\n\nLink to: " . $meta['romhacks_page'];
|
||||
}
|
||||
|
||||
$newsId = DB::table('news')
|
||||
->insertGetId([
|
||||
'title' => $post->post_title,
|
||||
'slug' => $slug,
|
||||
'category_id' => $categoryId,
|
||||
'description' => $description,
|
||||
'state' => self::STATE_MAP[$post->post_status],
|
||||
'entry_id' => null,
|
||||
'relevant_link' => $meta['release_site'] ?? null,
|
||||
'youtube_link' => $meta['youtube_video'] ?? null,
|
||||
'user_id' => $userId,
|
||||
'created_at' => $post->post_date,
|
||||
'updated_at' => $post->post_modified,
|
||||
]);
|
||||
|
||||
DB::table('migrations_logs')->insert([
|
||||
'source_system' => 'wp',
|
||||
'source_table' => 'wp_posts__news',
|
||||
'source_id' => $post->ID,
|
||||
'target_table' => 'news',
|
||||
'target_id' => $newsId,
|
||||
'status' => 'done',
|
||||
'migrated_at' => now(),
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$this->stats['created']++;
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$this->stats = [ 'created' => 0, 'missing_author' => 0, 'missing_category' => 0 ];
|
||||
|
||||
$query = DB::connection('old_wp')
|
||||
->table('posts')
|
||||
->where('post_type', 'news')
|
||||
->whereIn('post_status', array_keys(self::STATE_MAP))
|
||||
;
|
||||
|
||||
if( $limit = $this->option('limit') ) {
|
||||
$query->limit((int)$limit);
|
||||
}
|
||||
|
||||
$posts = $query->select('ID', 'post_title', 'post_name', 'post_content', 'post_author', 'post_status', 'post_date', 'post_modified')->get();
|
||||
|
||||
$this->info( "{$posts->count()} posts found" );
|
||||
|
||||
$this->withProgressBar($posts, fn($post) => $this->migrateNews($post) );
|
||||
|
||||
$this->newLine();
|
||||
$this->info( "Created {$this->stats['created']} posts. Missing authors: {$this->stats['missing_author']}. Missing category: {$this->stats['missing_category']}" );
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
128
app/Console/Commands/MigrateNewsImages.php
Normal file
128
app/Console/Commands/MigrateNewsImages.php
Normal file
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Attributes\Description;
|
||||
use Illuminate\Console\Attributes\Signature;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
#[Signature('migrate:news:images {--wp-uploads-path=} {--limit=}')]
|
||||
#[Description("Migrate WP galeries for news.")]
|
||||
class MigrateNewsImages extends Command
|
||||
{
|
||||
private const string GALLERYABLE_TYPE = 'App\\Models\\News';
|
||||
|
||||
private function copyAttachment( int $attachmentId, string $wpUploadsPath, string $destinationPath ): ?string
|
||||
{
|
||||
$relativePath = DB::connection('old_wp')
|
||||
->table('postmeta')
|
||||
->where('post_id', $attachmentId)
|
||||
->where('meta_key', '_wp_attached_file')
|
||||
->value('meta_value');
|
||||
|
||||
if( !$relativePath )
|
||||
return null;
|
||||
|
||||
$sourcePath = rtrim($wpUploadsPath, '/') . '/' . $relativePath;
|
||||
if( !is_file($sourcePath) )
|
||||
return null;
|
||||
|
||||
$extension = pathinfo($sourcePath, PATHINFO_EXTENSION);
|
||||
$filename = Str::random(40) . ($extension ? '.' . $extension : '');
|
||||
$destinationRelative = $destinationPath . '/' . $filename;
|
||||
|
||||
$stream = fopen($sourcePath, 'r');
|
||||
Storage::disk('public')->put($destinationRelative, $stream);
|
||||
if( is_resource($stream) )
|
||||
fclose($stream);
|
||||
|
||||
return $destinationRelative;
|
||||
}
|
||||
|
||||
private function processEntry( int $wpPostId, int $entryId, string $wpUploadsPath, array &$stats ): void
|
||||
{
|
||||
$exists = DB::table('migrations_logs')
|
||||
->where('source_system', 'wp')
|
||||
->where('source_table', 'wp_posts__attachments')
|
||||
->where('source_id', $wpPostId)
|
||||
->exists();
|
||||
|
||||
if( $exists )
|
||||
return;
|
||||
|
||||
$galleryRaw = DB::connection('old_wp')
|
||||
->table('postmeta')
|
||||
->where('post_id', $wpPostId)
|
||||
->where('meta_key', 'my_gallery')
|
||||
->value('meta_value');
|
||||
|
||||
$attachmentIds = $galleryRaw ? (@unserialize($galleryRaw) ?: []) : [];
|
||||
|
||||
foreach ( array_values( $attachmentIds ) as $order => $attachmentId ) {
|
||||
$newPath = $this->copyAttachment( (int) $attachmentId, $wpUploadsPath, "news/gallery-images/{$entryId}" );
|
||||
if( !$newPath ){
|
||||
$stats['missing_files']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
DB::table('galleries')
|
||||
->insert([
|
||||
'galleryable_type' => self::GALLERYABLE_TYPE,
|
||||
'galleryable_id' => $entryId,
|
||||
'image' => $newPath,
|
||||
'order' => $order,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
$stats['gallery_images']++;
|
||||
}
|
||||
|
||||
DB::table('migrations_logs')
|
||||
->insert([
|
||||
'source_system' => 'wp',
|
||||
'source_table' => 'wp_posts__attachments',
|
||||
'source_id' => $wpPostId,
|
||||
'target_table' => 'news',
|
||||
'target_id' => $entryId,
|
||||
'status' => 'done',
|
||||
'migrated_at' => now(),
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$wpUploadsPath = $this->option('wp-uploads-path');
|
||||
if( !$wpUploadsPath || !is_dir($wpUploadsPath) ){
|
||||
$this->error('Missing WP Uploads Path');
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$query = DB::table('migrations_logs')
|
||||
->where('source_system', 'wp')
|
||||
->where('source_table', 'wp_posts__news')
|
||||
->where('target_table', 'news')
|
||||
;
|
||||
|
||||
if( $limit = $this->option('limit') ){
|
||||
$query->limit((int)$limit);
|
||||
}
|
||||
|
||||
$rows = $query->get(['source_id', 'target_id']);
|
||||
$this->info("{$rows->count()} news need to be migrated");
|
||||
|
||||
$stats = ['gallery_images' => 0, 'missing_files' => 0 ];
|
||||
|
||||
$this->withProgressBar($rows, function($row) use($wpUploadsPath, &$stats) {
|
||||
$this->processEntry( $row->source_id, $row->target_id, $wpUploadsPath, $stats );
|
||||
});
|
||||
|
||||
$this->newLine();
|
||||
$this->info("Migrated attachments Galleries: {$stats['gallery_images']}, Missing files: {$stats['missing_files']}");
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
117
app/Console/Commands/MigrateReviewsExecute.php
Normal file
117
app/Console/Commands/MigrateReviewsExecute.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Helpers\MigrationHelpers;
|
||||
use Illuminate\Console\Attributes\Signature;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
#[Signature('migrate:reviews:execute {--limit=}')]
|
||||
class MigrateReviewsExecute extends Command
|
||||
{
|
||||
|
||||
private function migrateReview( $post, array &$stats ): void
|
||||
{
|
||||
$exists = DB::table('migrations_logs')
|
||||
->where('source_system', 'wp')
|
||||
->where('source_table', 'wp_posts__reviews')
|
||||
->where('source_id', $post->ID )
|
||||
->exists();
|
||||
|
||||
if( $exists )
|
||||
return;
|
||||
|
||||
$meta = DB::connection('old_wp')
|
||||
->table('postmeta')
|
||||
->where('post_id', $post->ID)
|
||||
->whereIn('meta_key', ['reviews_post_link', 'review_rating'])
|
||||
->pluck('meta_value', 'meta_key')
|
||||
;
|
||||
|
||||
$linkedWpPostId = $meta['reviews_post_link'] ?? null;
|
||||
if( !$linkedWpPostId ){
|
||||
$stats['missing_entry']++;
|
||||
return;
|
||||
}
|
||||
|
||||
$entryId = DB::table('migrations_logs')
|
||||
->where('source_system', 'wp')
|
||||
->where('source_table', 'wp_posts')
|
||||
->where('source_id', (int) $linkedWpPostId )
|
||||
->where('target_table', 'entries')
|
||||
->value('target_id');
|
||||
|
||||
if( !$entryId ){
|
||||
$stats['missing_entry']++;
|
||||
return;
|
||||
}
|
||||
|
||||
$userId = DB::table('migration_user_plan')
|
||||
->where('wp_user_id', $post->post_author )
|
||||
->value('user_id');
|
||||
|
||||
if( !$userId ){
|
||||
$stats['missing_author']++;
|
||||
return;
|
||||
}
|
||||
|
||||
$rating = $meta['review_rating'] ?? null;
|
||||
if( $rating === null || $rating === '' ){
|
||||
$stats['missing_rating']++;
|
||||
return;
|
||||
}
|
||||
|
||||
$description = trim($post->post_content) === '' ? '' : MigrationHelpers::htmlToMarkdown($post->post_content);
|
||||
|
||||
$reviewId = DB::table('entry_reviews')->insertGetId([
|
||||
'entry_id' => $entryId,
|
||||
'title' => $post->post_title,
|
||||
'rating' => (int) $rating,
|
||||
'description' => $description,
|
||||
'user_id' => $userId,
|
||||
'created_at' => $post->post_date,
|
||||
'updated_at' => $post->post_modified,
|
||||
]);
|
||||
|
||||
DB::table('migrations_logs')->insert([
|
||||
'source_system' => 'wp',
|
||||
'source_table' => 'wp_posts__reviews',
|
||||
'source_id' => $post->ID,
|
||||
'target_table' => 'entry_reviews',
|
||||
'target_id' => $reviewId,
|
||||
'status' => 'done',
|
||||
'migrated_at' => now(),
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$stats['created']++;
|
||||
}
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$query = DB::connection('old_wp')
|
||||
->table('posts')
|
||||
->where('post_type', 'reviews')
|
||||
->where('post_status', 'publish')
|
||||
;
|
||||
|
||||
if( $limit = $this->option('limit') ) {
|
||||
$query->limit((int) $limit);
|
||||
}
|
||||
|
||||
$posts = $query->select('ID', 'post_title', 'post_content', 'post_author', 'post_date', 'post_modified' )->get();
|
||||
$this->info("{$posts->count()} reviews found.");
|
||||
|
||||
$stats = ['created' => 0, 'missing_entry' => 0, 'missing_author' => 0, 'missing_rating' => 0 ];
|
||||
|
||||
$this->withProgressBar($posts, function($post) use (&$stats){
|
||||
$this->migrateReview($post, $stats);
|
||||
});
|
||||
|
||||
$this->newLine();
|
||||
$this->info("{$stats['created']} reviews created. {$stats['missing_entry']} missing entry. {$stats['missing_author']} missing author. {$stats['missing_rating']} missing rating.");
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
44
app/Console/Commands/MigrateTaxonomiesConfigure.php
Normal file
44
app/Console/Commands/MigrateTaxonomiesConfigure.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Attributes\Description;
|
||||
use Illuminate\Console\Attributes\Signature;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
#[Signature('migrate:taxonomies:configure')]
|
||||
#[Description('Configure taxonomies table.')]
|
||||
class MigrateTaxonomiesConfigure extends Command
|
||||
{
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$taxonomyMap = [];
|
||||
$taxonomy = "a";
|
||||
$tableName = "";
|
||||
|
||||
while( $taxonomy !== "" ){
|
||||
$taxonomy = "";
|
||||
$tableName = "";
|
||||
|
||||
$taxonomy = $this->ask("Write WP taxonomy name. Write nothing if you want to save changes.", "" );
|
||||
if( $taxonomy == "" )
|
||||
break;
|
||||
|
||||
$tableName = $this->ask("Write equivalent Laravel table name.");
|
||||
|
||||
$taxonomyMap[$taxonomy] = $tableName;
|
||||
}
|
||||
|
||||
DB::table('migration_settings')
|
||||
->updateOrInsert([
|
||||
'key' => 'wp_taxonomies_to_laravel_tables'
|
||||
],[
|
||||
'value' => json_encode($taxonomyMap), 'updated_at' => now()
|
||||
]);
|
||||
|
||||
$this->info('WP Taxonomies to Laravel tables have been configured.');
|
||||
}
|
||||
}
|
||||
77
app/Console/Commands/MigrateTaxonomiesExecute.php
Normal file
77
app/Console/Commands/MigrateTaxonomiesExecute.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Attributes\Description;
|
||||
use Illuminate\Console\Attributes\Signature;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
#[Signature('migrate:taxonomies:execute')]
|
||||
#[Description('Migrate taxonomies, make sure you have configured the migration before that.')]
|
||||
class MigrateTaxonomiesExecute extends Command
|
||||
{
|
||||
public function handle()
|
||||
{
|
||||
$taxMap = json_decode(DB::table('migration_settings')->where('key', 'wp_taxonomies_to_laravel_tables')->value('value'), true);
|
||||
|
||||
if( !$taxMap ){
|
||||
$this->error("No WP taxonomies need to be transferred.");
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
if( $this->ask("Are you sure you want launch that migration? Write ok if you want to launch it.", "") !== 'ok' ){
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
foreach ( $taxMap as $wpTax => $table )
|
||||
{
|
||||
$this->info("Migrate: {$wpTax} => {$table}");
|
||||
|
||||
$terms = DB::connection('old_wp')
|
||||
->table('term_taxonomy')
|
||||
->join('terms', 'term_taxonomy.term_id', '=', 'terms.term_id')
|
||||
->where('term_taxonomy.taxonomy', $wpTax)
|
||||
->select('term_taxonomy.term_taxonomy_id', 'terms.name', 'terms.slug')
|
||||
->get();
|
||||
|
||||
$this->withProgressBar($terms, function ($term) use ($table) {
|
||||
|
||||
$exists = DB::table('migrations_logs')
|
||||
->where('source_system', 'wp')
|
||||
->where('source_table', 'wp_term_taxonomy')
|
||||
->where('source_id', $term->term_taxonomy_id )
|
||||
->exists();
|
||||
|
||||
if( $exists )
|
||||
return;
|
||||
|
||||
$Id = DB::table( $table )->where('slug', $term->slug)->value('id');
|
||||
|
||||
if( $Id === null) {
|
||||
$Id = DB::table($table)
|
||||
->insertGetId([
|
||||
'name' => $term->name, 'slug' => $term->slug,
|
||||
'created_at' => now(), 'updated_at' => now()
|
||||
]);
|
||||
}
|
||||
|
||||
DB::table('migrations_logs')->insert([
|
||||
'source_system' => 'wp',
|
||||
'source_table' => 'wp_term_taxonomy',
|
||||
'source_id' => $term->term_taxonomy_id,
|
||||
'target_table' => $table,
|
||||
'target_id' => $Id,
|
||||
'status' => 'done',
|
||||
'migrated_at' => now(),
|
||||
'created_at' => now(),
|
||||
'updated_at' => now()
|
||||
]);
|
||||
});
|
||||
$this->newLine();
|
||||
}
|
||||
|
||||
$this->info("Migration done");
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
57
app/Console/Commands/MigrateUsersConfigure.php
Normal file
57
app/Console/Commands/MigrateUsersConfigure.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Attributes\Description;
|
||||
use Illuminate\Console\Attributes\Signature;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
#[Signature('migrate:users:configure')]
|
||||
#[Description('Configure users migrations settings like roles.')]
|
||||
class MigrateUsersConfigure extends Command
|
||||
{
|
||||
|
||||
private function getWpRoles(): array
|
||||
{
|
||||
$wpRoles = ['administrator', 'editor', 'author', 'contributor', 'subscriber'];
|
||||
$customWpRoles = ['bot'];
|
||||
|
||||
return array_merge($wpRoles, $customWpRoles);
|
||||
}
|
||||
|
||||
private function getOldXfGroups(): array|Collection
|
||||
{
|
||||
return DB::connection('old_xf')->table('user_group')->pluck('title', 'user_group_id');
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$wpRoles = $this->getWpRoles();
|
||||
$roleMap = [];
|
||||
foreach ($wpRoles as $role) {
|
||||
$roleMap[$role] = (int) $this->ask("XenForo group ID linked to role {$role}");
|
||||
}
|
||||
DB::table('migration_settings')
|
||||
->updateOrInsert([
|
||||
'key' => 'wp_role_to_xf_group'
|
||||
], [
|
||||
'value' => json_encode($roleMap), 'updated_at' => now()
|
||||
]);
|
||||
|
||||
$oldXfGroups = $this->getOldXfGroups();
|
||||
$groupMap = [];
|
||||
foreach ($oldXfGroups as $id => $title) {
|
||||
$groupMap[$id] = (int) $this->ask("Xenforo group ID linked to old XF group {$title}|{$id}");
|
||||
}
|
||||
DB::table('migration_settings')
|
||||
->updateOrInsert([
|
||||
'key' => 'old_xf_group_to_xf_group'
|
||||
],[
|
||||
'value' => json_encode($groupMap), 'updated_at' => now()
|
||||
]);
|
||||
|
||||
$this->info('XF groups updated.');
|
||||
}
|
||||
}
|
||||
229
app/Console/Commands/MigrateUsersExecute.php
Normal file
229
app/Console/Commands/MigrateUsersExecute.php
Normal file
@@ -0,0 +1,229 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\MigrationUserPlan;
|
||||
use App\Services\XenforoApiService;
|
||||
use Illuminate\Console\Attributes\Description;
|
||||
use Illuminate\Console\Attributes\Signature;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
#[Signature('migrate:users:execute {--limit=} {--wp-uploads-path=} {--xf-data-path=}')]
|
||||
#[Description("Migrate all users in migration table to XenForo.")]
|
||||
class MigrateUsersExecute extends Command
|
||||
{
|
||||
|
||||
private function extractWpRole(?string $serialized): string
|
||||
{
|
||||
$caps = $serialized ? @unserialize($serialized) : null;
|
||||
return is_array($caps) ? (array_key_first($caps) ?: 'contributor') : 'contributor';
|
||||
}
|
||||
|
||||
private function extractWpAvatarPath(?string $serialized, string $wpUploadsPath ): ?string
|
||||
{
|
||||
$data = $serialized ? @unserialize($serialized) : null;
|
||||
$mediaId = $data['media_id'] ?? null;
|
||||
if( !$mediaId )
|
||||
return null;
|
||||
|
||||
$relative = DB::connection('old_wp')
|
||||
->table('postmeta')
|
||||
->where('post_id', $mediaId)
|
||||
->where('meta_key', '_wp_attached_file')
|
||||
->value('meta_value');
|
||||
|
||||
if( !$relative )
|
||||
return null;
|
||||
|
||||
$path = rtrim($wpUploadsPath, '/') . '/' . $relative;
|
||||
return is_file($path) ? $path : null;
|
||||
}
|
||||
|
||||
private function extractXfAvatarPath(int $userId, string $xfDataPath, string $size = 'o' ): ?string
|
||||
{
|
||||
$path = sprintf('%s/avatars/%s/%d/%d.jpg',
|
||||
rtrim($xfDataPath, '/'), $size, (int) floor($userId / 1000), $userId
|
||||
);
|
||||
return is_file($path) ? $path : null;
|
||||
}
|
||||
|
||||
private function extractXfBannerPath(int $userId, string $xfDataPath, string $size = 'l' ): ?string
|
||||
{
|
||||
$path = sprintf('%s/profile_banners/%s/%d/%d.jpg',
|
||||
rtrim($xfDataPath, '/'), $size, (int) floor($userId / 1000), $userId
|
||||
);
|
||||
return is_file($path) ? $path : null;
|
||||
}
|
||||
|
||||
private function buildUserInfos( MigrationUserPlan $plan, array $roleMap, array $groupMap, string $wpUploadsPath, string $xfDataPath ): array
|
||||
{
|
||||
$infos = [
|
||||
'username' => $plan->xf_username ?: $plan->wp_username,
|
||||
'email' => $plan->email,
|
||||
'user_group_id' => null,
|
||||
'password' => Str::uuid(), // Used for API verifications.
|
||||
'xf_user_id' => 0,
|
||||
'avatar_path' => null,
|
||||
'banner_path' => null,
|
||||
'register_date' => null,
|
||||
'profile' => [
|
||||
'about' => null,
|
||||
'website' => null,
|
||||
],
|
||||
'source_real_password' => null,
|
||||
'wp_password' => null,
|
||||
'xf_scheme_class' => null,
|
||||
'xf_password_data' => null,
|
||||
];
|
||||
|
||||
if( $plan->wp_user_id )
|
||||
{
|
||||
$wp = DB::connection('old_wp')
|
||||
->table('users')
|
||||
->leftJoin('usermeta as m1', fn( $j ) => $j->on('users.ID', '=', 'm1.user_id')->where('m1.meta_key', '=', 'description') )
|
||||
->leftJoin('usermeta as m2', fn( $j ) => $j->on('users.ID', '=', 'm2.user_id')->where('m2.meta_key', '=', 'wp_capabilities') )
|
||||
->leftJoin('usermeta as m3', fn( $j ) => $j->on('users.ID', '=', 'm3.user_id')->where('m3.meta_key', '=', 'simple_local_avatar') )
|
||||
->where('users.ID', '=', $plan->wp_user_id )
|
||||
->select('m1.meta_value as description', 'm2.meta_value as capabilities', 'm3.meta_value as avatar_meta', 'users.user_url as website', 'users.user_pass as password', 'users.user_registred' )
|
||||
->first();
|
||||
|
||||
$infos['register_date'] = $wp->user_registred ? strtotime($wp->user_registred) : null;
|
||||
$infos['profile']['about'] = $wp->description;
|
||||
$infos['profile']['website'] = $wp->website;
|
||||
$role = $this->extractWpRole($wp->capabilities);
|
||||
$infos['user_group_id'] = $roleMap[$role] ?? $roleMap['contributor'];
|
||||
|
||||
if( $url = $this->extractWpAvatarPath($wp->avatar_meta, $wpUploadsPath)){
|
||||
$infos['avatar_path'] = $url;
|
||||
}
|
||||
|
||||
$infos['source_real_password'] = 'wp';
|
||||
$infos['wp_password'] = $wp->password;
|
||||
}
|
||||
|
||||
if( $plan->xf_user_id )
|
||||
{
|
||||
$xf = DB::connection('old_xf')
|
||||
->table('user')
|
||||
->leftJoin('user_profile', 'user.user_id', '=', 'user_profile.user_id')
|
||||
->leftJoin('user_authenticate', 'user.user_id', '=', 'user_authenticate.user_id')
|
||||
->where('user.user_id', '=', $plan->xf_user_id)
|
||||
->select('user.avatar_date', 'user.user_group_id', 'user.register_date', 'user_profile.about', 'user_profile.website', 'user_profile.banner_date', 'user_authenticate.scheme_class', 'user_authenticate.data')
|
||||
->first();
|
||||
|
||||
if( !$infos['register_date'] && $xf )
|
||||
$infos['register_date'] = $xf->register_date ?: null;
|
||||
if( !$infos['profile']['about'] && $xf )
|
||||
$infos['profile']['about'] = $xf->about ?: null;
|
||||
if( !$infos['profile']['website'] && $xf )
|
||||
$infos['profile']['website'] = $xf->website ?: null;
|
||||
|
||||
if( !$plan->wp_user_id ){
|
||||
$infos['user_group_id'] = $groupMap[$xf->user_group_id] ?? reset($groupMap);
|
||||
}
|
||||
$infos['xf_user_id'] = $plan->xf_user_id;
|
||||
|
||||
if( $infos['avatar_path'] === null && (int) $xf->avatar_date > 0){
|
||||
if( $path = $this->extractXfAvatarPath($plan->xf_user_id, $xfDataPath)){
|
||||
$infos['avatar_path'] = $path;
|
||||
}
|
||||
}
|
||||
|
||||
if( $infos['banner_path'] === null && (int) $xf->banner_date > 0 ){
|
||||
if( $path = $this->extractXfBannerPath($plan->xf_user_id, $xfDataPath)){
|
||||
$infos['banner_path'] = $path;
|
||||
}
|
||||
}
|
||||
|
||||
if( $infos['source_real_password'] === null && $xf->scheme_class && $xf->data ){
|
||||
$infos['source_real_password'] = 'xf';
|
||||
$infos['xf_scheme_class'] = $xf->scheme_class;
|
||||
$infos['xf_password_data'] = $xf->data;
|
||||
}
|
||||
}
|
||||
|
||||
return $infos;
|
||||
}
|
||||
|
||||
private function logMap( string $sourceSystem, string $sourceTable, int $sourceId, int $targetId )
|
||||
{
|
||||
DB::table('migrations_logs')->insert([
|
||||
'source_system' => $sourceSystem,
|
||||
'source_table' => $sourceTable,
|
||||
'source_id' => $sourceId,
|
||||
'target_table' => 'xf_user',
|
||||
'target_id' => $targetId,
|
||||
'status' => 'done',
|
||||
'migrated_at' => now(),
|
||||
'created_at' => now(),
|
||||
'updated_at' => now()
|
||||
]);
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$wpUploadsPath = $this->option('wp-uploads-path');
|
||||
$xfDataPath = $this->option('xf-data-path');
|
||||
|
||||
if( !$wpUploadsPath || !is_dir($wpUploadsPath) ){
|
||||
$this->error('Missing WP Uploads Path');
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
if( !$xfDataPath || !is_dir($xfDataPath) ){
|
||||
$this->error('Missing XF Data Path');
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$roleMap = json_decode(DB::table('migration_settings')->where('key', 'wp_role_to_xf_group')->value('value'), true);
|
||||
$groupMap = json_decode(DB::table('migration_settings')->where('key', 'old_xf_group_to_xf_group')->value('value'), true);
|
||||
|
||||
if( !$roleMap || !$groupMap ) {
|
||||
$this->error('Role map and group map are required.');
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$query = MigrationUserPlan::where('status', 'approved')->whereNull('user_id');
|
||||
if( $limit = $this->option('limit') ) {
|
||||
$query->limit((int) $limit);
|
||||
}
|
||||
|
||||
$rows = $query->get();
|
||||
$this->info("{$rows->count()} accounts will be created on XenForo database !!!.");
|
||||
$ok = $this->ask("Write 'ok' if you want to start the migration. Everything else to quit the migration.");
|
||||
if( $ok !== 'ok' ) {
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$service = app(XenforoApiService::class);
|
||||
|
||||
$this->withProgressBar($rows, function($plan) use( $roleMap, $groupMap, $wpUploadsPath, $xfDataPath, $service ) {
|
||||
try {
|
||||
$infos = $this->buildUserInfos( $plan, $roleMap, $groupMap, $wpUploadsPath, $xfDataPath );
|
||||
|
||||
[ $userId, $passwordSet ] = $service->_migrateUser( $infos );
|
||||
if( !$userId ){
|
||||
throw new \RuntimeException("Error when user creation.");
|
||||
}
|
||||
|
||||
MigrationUserPlan::where('id', $plan->id )->update([ 'user_id' => $userId ]);
|
||||
|
||||
$this->logMap( $plan->wp_user_id ? 'wp' : 'xf', $plan->wp_user_id ? 'wp_users' : 'xf_user', $plan->wp_user_id ?? $plan->xf_user_id, $userId );
|
||||
if( $plan->wp_user_id && $plan->xf_user_id ){
|
||||
$this->logMap('xf', 'xf_user', $plan->xf_user_id, $userId );
|
||||
}
|
||||
|
||||
|
||||
} catch ( \Throwable $e ) {
|
||||
Log::error("Unable to create Plan#{$plan->id} user : {$e->getMessage()}");
|
||||
}
|
||||
});
|
||||
|
||||
$this->newLine(2);
|
||||
$this->info("Process finished.");
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
143
app/Console/Commands/MigrateUsersPlan.php
Normal file
143
app/Console/Commands/MigrateUsersPlan.php
Normal file
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\MigrationUserPlan;
|
||||
use Illuminate\Console\Attributes\Description;
|
||||
use Illuminate\Console\Attributes\Signature;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
#[Signature('migrate:users:plan {--fresh}')]
|
||||
#[Description('Construct migration plan for WP/XF accounts.')]
|
||||
class MigrateUsersPlan extends Command
|
||||
{
|
||||
public function handle()
|
||||
{
|
||||
if( $this->option('fresh') ) {
|
||||
MigrationUserPlan::truncate();
|
||||
}
|
||||
|
||||
$this->info("Loading old XF accounts...");
|
||||
|
||||
$xfUsers = DB::connection('old_xf')->table('user')
|
||||
->select('user_id','username','email')
|
||||
->get()
|
||||
->keyBy('user_id')
|
||||
;
|
||||
|
||||
$xfUsersByEmail = $xfUsers->groupBy(fn($u) => strtolower($u->email));
|
||||
|
||||
$this->info("Loading old WP accounts...");
|
||||
|
||||
$wpUsers = DB::connection('old_wp')->table('users')
|
||||
->leftJoin('usermeta', function ($join) {
|
||||
$join->on('users.ID', '=', 'usermeta.user_id')->where('usermeta.meta_key', '=', 'xf_user_id');
|
||||
})
|
||||
->select('users.ID as wp_id', 'users.user_email as email', 'users.user_login as username', 'usermeta.meta_value as linked_xf_id')
|
||||
->get();
|
||||
|
||||
$linkedXfIds = [];
|
||||
|
||||
$this->withProgressBar( $wpUsers, function ($wp) use ($xfUsers, $xfUsersByEmail, &$linkedXfIds) {
|
||||
$email = strtolower($wp->email);
|
||||
$linkedId = $wp->linked_xf_id ? (int) $wp->linked_xf_id : null;
|
||||
|
||||
if( $linkedId && $xfUsers->has($linkedId) ) {
|
||||
|
||||
$xf = $xfUsers->get($linkedId);
|
||||
$matchType = strtolower($xf->email) === $email ? 'explicit' : 'conflict';
|
||||
|
||||
MigrationUserPlan::updateOrCreate(
|
||||
['wp_user_id' => $wp->wp_id],
|
||||
[
|
||||
'xf_user_id' => $linkedId,
|
||||
'match_type' => $matchType,
|
||||
'email' => $email,
|
||||
'wp_username' => $wp->username,
|
||||
'xf_username' => $xf->username,
|
||||
'note' => $matchType === 'conflict' ? "E-Mail différent: {$xf->email}" : null,
|
||||
'status' => $matchType === 'explicit' ? 'approved' : 'pending',
|
||||
]
|
||||
);
|
||||
$linkedXfIds[$linkedId] = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if( $linkedId && !$xfUsers->has($linkedId) ) {
|
||||
MigrationUserPlan::updateOrCreate(
|
||||
['wp_user_id' => $wp->wp_id ],
|
||||
[
|
||||
'match_type' => 'conflict',
|
||||
'email' => $email,
|
||||
'wp_username' => $wp->username,
|
||||
'note' => "xf_user_id={$linkedId} introuvable",
|
||||
'status' => 'approved'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$candidates = $xfUsersByEmail->get($email,collect());
|
||||
|
||||
if( $candidates->count() === 1 ){
|
||||
$xf = $candidates->first();
|
||||
MigrationUserPlan::updateOrCreate(
|
||||
['wp_user_id' => $wp->wp_id],
|
||||
[
|
||||
'xf_user_id' => $xf->user_id,
|
||||
'match_type' => 'email',
|
||||
'email' => $email,
|
||||
'wp_username' => $wp->username,
|
||||
'xf_username' => $xf->username,
|
||||
'status' => 'pending'
|
||||
]
|
||||
);
|
||||
$linkedXfIds[$xf->user_id] = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if( $candidates->count() > 1 ){
|
||||
MigrationUserPlan::updateOrCreate(
|
||||
['wp_user_id' => $wp->wp_id ],
|
||||
[
|
||||
'match_type' => 'conflict',
|
||||
'email' => $email,
|
||||
'wp_username' => $wp->username,
|
||||
'note' => "E-mail identique sur plusieurs comptes XF.",
|
||||
]
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
MigrationUserPlan::updateOrCreate(
|
||||
['wp_user_id' => $wp->wp_id ],
|
||||
[
|
||||
'match_type' => 'wp_only',
|
||||
'email' => $email,
|
||||
'wp_username' => $wp->username,
|
||||
'status' => 'approved'
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
$this->info("Listing old XF accounts...");
|
||||
foreach( $xfUsers as $id => $xf ) {
|
||||
if( isset($linkedXfIds[$id]) ) {
|
||||
continue;
|
||||
}
|
||||
MigrationUserPlan::updateOrCreate(
|
||||
['xf_user_id' => $xf->user_id, 'wp_user_id' => null],
|
||||
[
|
||||
'match_type' => 'xf_only',
|
||||
'email' => strtolower($xf->email),
|
||||
'xf_username' => $xf->username,
|
||||
'status' => 'approved'
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
$pending = MigrationUserPlan::where('status', 'pending')->count();
|
||||
$this->newLine(2);
|
||||
$this->info("Plan generated. {$pending} pending cases.");
|
||||
}
|
||||
}
|
||||
41
app/Console/Commands/MigrateXFConfigure.php
Normal file
41
app/Console/Commands/MigrateXFConfigure.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Attributes\Description;
|
||||
use Illuminate\Console\Attributes\Signature;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
#[Signature('migrate:xf:configure')]
|
||||
#[Description("Configure forums map for migrating threads.")]
|
||||
class MigrateXFConfigure extends Command
|
||||
{
|
||||
public function handle()
|
||||
{
|
||||
$forums = DB::connection('old_xf')
|
||||
->table('node')
|
||||
->where('node_type_id', 'Forum')
|
||||
->select('node_id', 'title')
|
||||
->get()
|
||||
;
|
||||
|
||||
$this->info("{$forums->count()} forums found.");
|
||||
|
||||
$forumMap = [];
|
||||
foreach ($forums as $forum) {
|
||||
$newId = $this->ask("New ID for {$forum->title}");
|
||||
if( $newId !== null && $newId !== '' ){
|
||||
$map[$forum->node_id] = (int) $newId;
|
||||
}
|
||||
}
|
||||
|
||||
DB::table('migration_settings')
|
||||
->updateOrInsert(
|
||||
[ 'key' => 'old_xf_node_to_new_xf_node'],
|
||||
[ 'value' => json_encode($map), 'updated_at' => now() ],
|
||||
);
|
||||
|
||||
$this->info("Config saved.");
|
||||
}
|
||||
}
|
||||
47
app/Helpers/MigrationHelpers.php
Normal file
47
app/Helpers/MigrationHelpers.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
use League\HTMLToMarkdown\HtmlConverter;
|
||||
|
||||
class MigrationHelpers
|
||||
{
|
||||
|
||||
public static function wpAutoP(string $content): string
|
||||
{
|
||||
$content = trim($content);
|
||||
if ($content === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$content = str_replace(["\r\n", "\r"], "\n", $content);
|
||||
$blocks = preg_split('/\n\s*\n/', $content);
|
||||
|
||||
$blocks = array_map(function ($block) {
|
||||
$block = trim($block);
|
||||
if ($block === '') {
|
||||
return '';
|
||||
}
|
||||
if (preg_match('/^<(p|div|table|ul|ol|blockquote|h[1-6]|pre|figure)[\s>]/i', $block)) {
|
||||
return $block;
|
||||
}
|
||||
return '<p>' . nl2br($block, false) . '</p>';
|
||||
}, $blocks);
|
||||
|
||||
return implode("\n\n", array_filter($blocks, fn ($b) => $b !== ''));
|
||||
}
|
||||
|
||||
public static function htmlToMarkdown( ?string $html ): string
|
||||
{
|
||||
if (!$html || trim($html) === '') {
|
||||
return $html;
|
||||
}
|
||||
|
||||
static $converter = null;
|
||||
if ($converter === null) {
|
||||
$converter = new HtmlConverter(['hard_break' => true]);
|
||||
}
|
||||
|
||||
return $converter->convert(self::wpAutoP($html));
|
||||
}
|
||||
}
|
||||
@@ -68,7 +68,7 @@ class DynamicLoadController extends Controller
|
||||
|
||||
public function activityFeed(Request $request): JsonResponse
|
||||
{
|
||||
$availableFilters = ['entries', 'news', 'messages', 'threads', 'clubs'];
|
||||
$availableFilters = ['entries', 'news', 'messages', 'threads', 'clubs', 'reviews'];
|
||||
|
||||
$requested = $request->query('filters')
|
||||
? explode(',', $request->query('filters'))
|
||||
|
||||
@@ -50,8 +50,9 @@ class EntryController extends Controller
|
||||
Gate::authorize($entryPolicy, $entry);
|
||||
|
||||
$comments = EntryHelpers::getLatestComments($entry);
|
||||
$reviews = $entry->reviews()->orderBy('created_at', 'desc')->limit(10)->get();
|
||||
|
||||
return view('entries.show', compact('entry', 'section', 'comments'));
|
||||
return view('entries.show', compact('entry', 'section', 'comments', 'reviews'));
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ class HomeController extends Controller
|
||||
|
||||
public function index( Request $request ): View {
|
||||
|
||||
$filters = [ 'entries', 'news', 'messages', 'threads', 'clubs' ];
|
||||
$filters = [ 'entries', 'news', 'messages', 'threads', 'clubs', 'reviews' ];
|
||||
|
||||
$cookie = $request->cookie('activity_filters');
|
||||
$activeFilters = $cookie ? array_intersect( json_decode( $cookie, true ) ?? [], $filters ) : $filters;
|
||||
@@ -32,6 +32,7 @@ class HomeController extends Controller
|
||||
'messages' => ['label' => 'Posts', 'icon' => 'message-square'],
|
||||
'threads' => ['label' => 'Threads', 'icon' => 'messages-square'],
|
||||
'clubs' => ['label' => 'Clubs', 'icon' => 'balloon'],
|
||||
'reviews' => ['label' => 'Reviews', 'icon' => 'star'],
|
||||
];
|
||||
|
||||
$latestNews = News::published()->latest('created_at')->limit(5)->get();
|
||||
|
||||
33
app/Http/Controllers/ReviewController.php
Normal file
33
app/Http/Controllers/ReviewController.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Exceptions\SubmissionException;
|
||||
use App\Http\Requests\StoreReviewRequest;
|
||||
use App\Models\Entry;
|
||||
use App\Services\ReviewsService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ReviewController extends Controller
|
||||
{
|
||||
|
||||
public function __construct(private ReviewsService $services){}
|
||||
|
||||
public function index(){
|
||||
|
||||
return view('reviews.index');
|
||||
}
|
||||
|
||||
|
||||
public function store(StoreReviewRequest $request, Entry $entry)
|
||||
{
|
||||
try {
|
||||
$this->services->storeReview( $request, $entry );
|
||||
return redirect()->route('entries.show', [ 'section' => $entry->type, 'entry' => $entry ])->with('success', "Your review has been published.");
|
||||
} catch ( SubmissionException $e ) {
|
||||
return back()->withInput()->withErrors(['error' => $e->getMessage()]);
|
||||
} catch ( \Exception $e ) {
|
||||
return back()->withInput()->withErrors(['error' => 'Unknown error: '.$e->getMessage()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,4 +61,10 @@ class ToolsController extends Controller
|
||||
|
||||
return view('tools.play', compact('patches', 'emuConfig'));
|
||||
}
|
||||
|
||||
public function hasher( Request $request )
|
||||
{
|
||||
return view('tools.hasher');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
50
app/Http/Middleware/CheckXenForoUserState.php
Normal file
50
app/Http/Middleware/CheckXenForoUserState.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class CheckXenForoUserState
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param Closure(Request): (Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
if( \Auth::guest() )
|
||||
return $next($request);
|
||||
|
||||
if( \Auth::user()->security_lock === 'change' )
|
||||
return $this->deny( $request, "Password must be changed." );
|
||||
else if( \Auth::user()->security_lock === 'reset' )
|
||||
return $this->deny( $request, "Password must be reset.");
|
||||
|
||||
if( \Auth::user()->user_state === 'valid' )
|
||||
return $next($request);
|
||||
|
||||
else if( \Auth::user()->user_state === 'email_confirm' || \Auth::user()->user_state === 'email_confirm_edit' )
|
||||
return $this->deny( $request, "You must verify your email address." );
|
||||
else if( \Auth::user()->user_state === 'email_bounce' )
|
||||
return $this->deny( $request, "Invalid email address." );
|
||||
else if( \Auth::user()->user_state === 'rejected' )
|
||||
return $this->deny( $request, "Your account is currently rejected." );
|
||||
else if( \Auth::user()->user_state === 'disabled' )
|
||||
return $this->deny( $request, "Your account is currently disabled." );
|
||||
|
||||
return $this->deny($request, "Invalid user state.");
|
||||
}
|
||||
|
||||
private function deny(Request $request, string $reason): Response
|
||||
{
|
||||
if($request->expectsJson())
|
||||
return \response()->json(['error' => 'forbidden', 'reason' => $reason], 403);
|
||||
|
||||
return response()->view('pages.user_state', [
|
||||
'reason' => $reason,
|
||||
], 403 );
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,7 @@ class StoreEntryRequest extends FormRequest
|
||||
if( section_must_be( ['romhacks', 'lua-scripts'], $section ) ){
|
||||
$rules['modifications'] = 'array|required|min:1';
|
||||
$rules['modifications.*'] = 'integer|exists:modifications,id';
|
||||
} else if( section_must_be( 'utilities', $section ) ){
|
||||
} else if( section_must_be( ['utilities','documents'], $section ) ){
|
||||
$rules['categories'] = 'array|required|min:1';
|
||||
$rules['categories.*'] = 'integer|exists:categories,id';
|
||||
}
|
||||
@@ -85,7 +85,7 @@ class StoreEntryRequest extends FormRequest
|
||||
|
||||
$rules['version'] = 'required|string|max:50';
|
||||
$rules['release-date'] = 'required|date';
|
||||
if( section_must_not_be( 'utilities', $section ) ){
|
||||
if( section_must_not_be( ['utilities', 'documents'], $section ) ){
|
||||
$rules['status'] = 'required|integer|exists:statuses,id';
|
||||
} else {
|
||||
$rules['level'] = 'required|integer|exists:levels,id';
|
||||
@@ -159,6 +159,7 @@ class StoreEntryRequest extends FormRequest
|
||||
$rules['owner_user_id'] = [ 'required', 'integer', new XfUserExists ];
|
||||
$rules['comments_thread_id'] = 'nullable|integer';
|
||||
$rules['featured'] = 'nullable|boolean';
|
||||
$rules['refresh_created_at'] = 'nullable|boolean';
|
||||
}
|
||||
|
||||
return $rules;
|
||||
@@ -167,16 +168,31 @@ class StoreEntryRequest extends FormRequest
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'entry_title.required' => 'Please provide an entry title.',
|
||||
'slug.unique' => 'An entry with this title already exists. Please choose another title.',
|
||||
'file_ids.required' => 'Please upload at least one file.',
|
||||
'file_ids.min' => 'Please upload at least one file.',
|
||||
'hashes.required' => 'Please add at least one hash.',
|
||||
'hashes.min' => 'Please add at least one hash.',
|
||||
'gallery.required' => 'Please add at least one screenshot.',
|
||||
'gallery.min' => 'Please add at least one screenshot.',
|
||||
'new-game-platform.required_without' => 'Please choose a platform for the new game.',
|
||||
'new-game-genre.required_without' => 'Please choose a genre for the new game.',
|
||||
'files_uuid.required' => 'Please upload at least a file.',
|
||||
'files_state.required' => 'A file may be corrupted, please reupload it.',
|
||||
'files_state.*.in' => 'A file state doesn\'t have a standard value.',
|
||||
'entry_title.required' => 'Please enter a title.',
|
||||
'modifications.required' => 'Please select at least one modification.',
|
||||
'categories.required' => 'Please select at least one category.',
|
||||
'system.required' => 'Please select at least one system.',
|
||||
'version.required' => 'Please enter a version number.',
|
||||
'release-date.required' => 'Please enter a valid release date.',
|
||||
'status.required' => 'Please select a status.',
|
||||
'level.required' => 'Please select an experience level.',
|
||||
'description.required' => 'Please enter a description.',
|
||||
'game_selection_mode.required' => 'Please select a game selection mode.',
|
||||
'platform_only_id.required' => 'Please select a platform.',
|
||||
'game_id.required' => 'Please select a game.',
|
||||
'new-game-title.required' => 'Please enter a game title.',
|
||||
'new-game-platform.required' => 'Please select a game platform.',
|
||||
'new-game-genre.required' => 'Please select a game genre.',
|
||||
'hashes.required' => 'Please send at least one hash.',
|
||||
'languages.required' => 'Please select at least one language.',
|
||||
'main-image.required' => 'Please upload a main image.',
|
||||
'gallery.required' => 'Please upload at least one image for the gallery.',
|
||||
'authors.required' => 'Please select at least one author.',
|
||||
'new-authors.required' => 'Please select at least one author.',
|
||||
'submit-state.required' => 'Please select a submit state.',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
41
app/Http/Requests/StoreReviewRequest.php
Normal file
41
app/Http/Requests/StoreReviewRequest.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreReviewRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
$review = $this->route('review');
|
||||
if( $review )
|
||||
return $this->user()->can('update', $review);
|
||||
|
||||
return $this->user()->can('create', '\App\Models\EntryReview');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
|
||||
$isEdit = (bool) $this->route('review');
|
||||
|
||||
$rules = [];
|
||||
|
||||
$rules['rating'] = 'required|numeric|min:1|max:5';
|
||||
$rules['title'] = 'required|string|max:255';
|
||||
$rules['description'] = 'required|string';
|
||||
|
||||
return $rules;
|
||||
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ class DeleteXenForoCommentsThread implements ShouldQueue
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(
|
||||
protected int $threadId,
|
||||
protected ?int $threadId,
|
||||
)
|
||||
{
|
||||
//
|
||||
@@ -29,6 +29,7 @@ class DeleteXenForoCommentsThread implements ShouldQueue
|
||||
*/
|
||||
public function handle(XenforoApiService $service): void
|
||||
{
|
||||
if( $this->threadId )
|
||||
$service->deleteThreadWithEntry($this->threadId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ class RestoreXenForoCommentsThread implements ShouldQueue
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(
|
||||
protected int $threadId,
|
||||
protected ?int $threadId,
|
||||
)
|
||||
{
|
||||
//
|
||||
@@ -29,6 +29,7 @@ class RestoreXenForoCommentsThread implements ShouldQueue
|
||||
*/
|
||||
public function handle(XenforoApiService $service): void
|
||||
{
|
||||
if( $this->threadId )
|
||||
$service->restoreThreadWithEntry($this->threadId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ class Database extends Component
|
||||
* Categories mode and/or
|
||||
* @var string
|
||||
*/
|
||||
#[Url(except:['or'])]
|
||||
#[Url(except:'or')]
|
||||
public string $categoriesMode = 'or';
|
||||
|
||||
/**
|
||||
@@ -142,7 +142,7 @@ class Database extends Component
|
||||
* Systems mode and/or
|
||||
* @var string
|
||||
*/
|
||||
#[Url(except:['or'])]
|
||||
#[Url(except:'or')]
|
||||
public string $systemsMode = 'or';
|
||||
|
||||
/**
|
||||
@@ -152,6 +152,9 @@ class Database extends Component
|
||||
#[Url(except:[])]
|
||||
public array $levels = [];
|
||||
|
||||
#[Url(except:null)]
|
||||
public ?int $userId = null;
|
||||
|
||||
/**
|
||||
* Sort by field.
|
||||
* @var string
|
||||
@@ -185,7 +188,6 @@ class Database extends Component
|
||||
'utilities' => 'Utilities',
|
||||
'documents' => 'Documents',
|
||||
'lua-scripts' => 'Lua Scripts',
|
||||
'tutorials' => 'Tutorials',
|
||||
];
|
||||
|
||||
public const int PAGINATION = 30;
|
||||
@@ -207,12 +209,14 @@ class Database extends Component
|
||||
public function updatedSystems(): void { $this->resetPage(); $this->dispatch('filters-updated'); }
|
||||
public function updatedSystemsMode(): void { $this->resetPage(); $this->dispatch('filters-updated'); }
|
||||
public function updatedLevels(): void { $this->resetPage(); $this->dispatch('filters-updated'); }
|
||||
public function updatedUserId(): void { $this->resetPage(); $this->dispatch('filters-updated'); }
|
||||
|
||||
public function clearFilters(): void
|
||||
{
|
||||
$this->reset([
|
||||
'search', 'types', 'platforms', 'genres', 'statuses', 'authors', 'authorsMode', 'languages', 'languagesMode', 'modifications', 'modificationsMode', 'categories', 'categoriesMode', 'systems', 'systemsMode', 'levels'
|
||||
'search', 'types', 'platforms', 'genres', 'statuses', 'authors', 'authorsMode', 'languages', 'languagesMode', 'modifications', 'modificationsMode', 'categories', 'categoriesMode', 'systems', 'systemsMode', 'levels', 'userId'
|
||||
]);
|
||||
$this->dispatch('filters-updated');
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
@@ -320,18 +324,59 @@ class Database extends Component
|
||||
}
|
||||
}
|
||||
|
||||
if( $this->userId ){
|
||||
$query->where('user_id', $this->userId);
|
||||
}
|
||||
|
||||
return $query->orderBy($this->sortBy, $this->sortDir);
|
||||
}
|
||||
|
||||
private function searchFilter( string $modelClass, string $search )
|
||||
{
|
||||
|
||||
$this->dispatch('filters-updated');
|
||||
|
||||
if( mb_strlen( $search ) < 3 ) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
return $modelClass::where('name', 'like', "%{$search}%")
|
||||
->orderBy('name')
|
||||
->limit(50)
|
||||
->get();
|
||||
}
|
||||
|
||||
private function searchGameFilter()
|
||||
{
|
||||
|
||||
$search = $this->gameSearch;
|
||||
|
||||
$this->dispatch('filters-updated');
|
||||
|
||||
if( mb_strlen( $search ) < 3 ) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
$collect = Game::where('name', 'like', "%{$search}%")
|
||||
->orderBy('name')
|
||||
->limit(50)
|
||||
->get();
|
||||
|
||||
return $collect->map(function($item){
|
||||
$item->name = $item->name . ' (' . ($item->platform?->short_name ?? $item->platform->name) . ')';
|
||||
return $item;
|
||||
} );
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.database', [
|
||||
'entries' => $this->buildQuery()->paginate(self::PAGINATION),
|
||||
'allGames' => Game::orderBy('name')->get(),
|
||||
'allGames' => $this->searchGameFilter(),
|
||||
'allPlatforms' => Platform::orderBy('name')->get(),
|
||||
'allGenres' => Genre::orderBy('name')->get(),
|
||||
'allStatuses' => Status::orderBy('name')->get(),
|
||||
'allAuthors' => Author::orderBy('name')->get(),
|
||||
'allAuthors' => $this->searchFilter(Author::class, $this->authorSearch),
|
||||
'allLanguages' => Language::orderBy('name')->get(),
|
||||
'allModifications' => Modification::orderBy('name')->get(),
|
||||
'allCategories' => Category::orderBy('name')->get(),
|
||||
|
||||
81
app/Livewire/HashesChecker.php
Normal file
81
app/Livewire/HashesChecker.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Helpers\HashesHelpers;
|
||||
use App\Models\EntryHash;
|
||||
use App\Models\Game;
|
||||
use App\Models\Genre;
|
||||
use App\Models\Platform;
|
||||
use Illuminate\View\View;
|
||||
use Livewire\Component;
|
||||
|
||||
/**
|
||||
* @phpstan-import-type HashObject from \App\Types\SubmissionTypes
|
||||
*/
|
||||
class HashesChecker extends Component
|
||||
{
|
||||
|
||||
/**
|
||||
* Current hash.
|
||||
* @var null|HashObject $hash
|
||||
*/
|
||||
public ?array $hash = null;
|
||||
|
||||
/**
|
||||
* Add a hash.
|
||||
*
|
||||
* @param string $filename
|
||||
* @param string $crc32
|
||||
* @param string $sha1
|
||||
* @param string|null $verified
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addHash(string $filename, string $crc32, string $sha1, ?string $verified = null): void
|
||||
{
|
||||
|
||||
if( $verified !== null && $verified !== "No" )
|
||||
$this->hash = [
|
||||
'filename' => $filename,
|
||||
'hash_crc32' => $crc32,
|
||||
'hash_sha1' => $sha1,
|
||||
'verified' => $verified
|
||||
];
|
||||
else if( ( $hash = HashesHelpers::findHashes( $sha1 ) ) !== null )
|
||||
$this->hash = [
|
||||
'filename' => $hash->filename,
|
||||
'hash_crc32' => $hash->crc32,
|
||||
'hash_sha1' => $hash->sha1,
|
||||
'verified' => HashesHelpers::getReferenceName( $hash->dat_reference_id )
|
||||
];
|
||||
else
|
||||
$this->hash = [
|
||||
'filename' => $filename,
|
||||
'hash_crc32' => $crc32,
|
||||
'hash_sha1' => $sha1,
|
||||
'verified' => "No"
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the hash.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function removeHash(): void
|
||||
{
|
||||
$this->hash = null;
|
||||
$this->dispatch('refresh');
|
||||
}
|
||||
|
||||
|
||||
public function render(): View
|
||||
{
|
||||
$entries = collect();
|
||||
if( $this->hash !== null ){
|
||||
$entries = EntryHash::where('hash_sha1', $this->hash['hash_sha1'] )->get()->pluck('entry')->filter();
|
||||
}
|
||||
return view('livewire.hashes-checker', compact('entries'));
|
||||
}
|
||||
}
|
||||
92
app/Livewire/Reviews.php
Normal file
92
app/Livewire/Reviews.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\EntryReview;
|
||||
use Livewire\Attributes\Url;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
|
||||
class Reviews extends Component
|
||||
{
|
||||
|
||||
use WithPagination;
|
||||
|
||||
#[Url(except:null)]
|
||||
public ?int $entryId = null;
|
||||
|
||||
#[Url(except:null)]
|
||||
public ?int $rating = null;
|
||||
|
||||
/**
|
||||
* Sort by field.
|
||||
* @var string
|
||||
*/
|
||||
#[Url(as: 'sort',except: 'created_at')]
|
||||
public string $sortBy = 'created_at';
|
||||
|
||||
/**
|
||||
* asc/desc
|
||||
* @var string
|
||||
*/
|
||||
#[Url(as: 'dir',except: 'desc')]
|
||||
public string $sortDir = 'desc';
|
||||
|
||||
/**
|
||||
* Translation of sort options key.
|
||||
*/
|
||||
public const array SORT_OPTIONS = [
|
||||
'created_at' => 'Date added',
|
||||
'rating' => 'Rating',
|
||||
'title' => 'Title'
|
||||
];
|
||||
|
||||
public const int PAGINATION = 30;
|
||||
|
||||
public function updatedEntryId(): void { $this->resetPage(); $this->dispatch('filters-updated'); }
|
||||
public function updatedRating(): void { $this->resetPage(); $this->dispatch('filters-updated'); }
|
||||
|
||||
public function clearFilters(): void
|
||||
{
|
||||
$this->reset([
|
||||
'entryId', 'rating'
|
||||
]);
|
||||
$this->dispatch('filters-updated');
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function setSort(string $field): void
|
||||
{
|
||||
if( $this->sortBy === $field ) {
|
||||
$this->sortDir = $this->sortDir === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
$this->sortBy = $field;
|
||||
$this->sortDir = 'asc';
|
||||
}
|
||||
$this->resetPage();
|
||||
$this->dispatch('filters-updated');
|
||||
}
|
||||
|
||||
private function buildQuery()
|
||||
{
|
||||
$query = EntryReview::query()->with([
|
||||
'entry'
|
||||
]);
|
||||
|
||||
if( $this->entryId ) {
|
||||
$query->where('entry_id', $this->entryId);
|
||||
}
|
||||
if( $this->rating ){
|
||||
$query->where('rating', $this->rating);
|
||||
}
|
||||
|
||||
return $query->orderBy($this->sortBy, $this->sortDir);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.reviews', [
|
||||
'reviews' => $this->buildQuery()->paginate(self::PAGINATION),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -4,12 +4,14 @@ namespace App\Models;
|
||||
|
||||
use App\Helpers\EntryHelpers;
|
||||
use App\Traits\HasGallery;
|
||||
use App\Traits\HasXenforoUserId;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use League\CommonMark\GithubFlavoredMarkdownConverter;
|
||||
use Monolog\Level;
|
||||
use Spatie\Activitylog\Models\Concerns\LogsActivity;
|
||||
use Spatie\Activitylog\Support\LogOptions;
|
||||
@@ -97,12 +99,17 @@ use Spatie\Activitylog\Support\LogOptions;
|
||||
* @method static Builder<static>|Entry withoutTrashed()
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Spatie\Activitylog\Models\Activity> $activitiesAsSubject
|
||||
* @property-read int|null $activities_as_subject_count
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\EntryReview> $reviews
|
||||
* @property-read int|null $reviews_count
|
||||
* @property-read float $average_rating
|
||||
* @property-read int $reviews_count_cached
|
||||
* @property-read string $description_html
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class Entry extends Model
|
||||
{
|
||||
|
||||
use SoftDeletes, HasGallery, LogsActivity;
|
||||
use SoftDeletes, HasGallery, LogsActivity, HasXenforoUserId;
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
@@ -143,6 +150,15 @@ class Entry extends Model
|
||||
'featured_at' => 'datetime',
|
||||
];
|
||||
|
||||
protected static function booted(): void {
|
||||
static::saving( function( $entry ) {
|
||||
if( $entry->isDirty('version') ) {
|
||||
$entry->created_at = now();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public function scopePublished( Builder $query ): Builder {
|
||||
return $query->where( 'state', 'published' );
|
||||
}
|
||||
@@ -207,6 +223,30 @@ class Entry extends Model
|
||||
return $this->hasMany(EntryHash::class);
|
||||
}
|
||||
|
||||
public function reviews(): HasMany {
|
||||
return $this->hasMany(EntryReview::class);
|
||||
}
|
||||
|
||||
public function getAverageRatingAttribute(): float
|
||||
{
|
||||
return round( $this->reviews->avg('rating') ?? 0, 1 );
|
||||
}
|
||||
|
||||
public function getReviewsCountCachedAttribute(): int
|
||||
{
|
||||
return $this->reviews->count();
|
||||
}
|
||||
|
||||
public function getDescriptionHtmlAttribute(): string
|
||||
{
|
||||
$converter = new GithubFlavoredMarkdownConverter([
|
||||
'html_input' => 'strip',
|
||||
'allow_unsafe_links' => false,
|
||||
]);
|
||||
|
||||
return $converter->convert($this->description)->getContent();
|
||||
}
|
||||
|
||||
public function parseStaffCredits(): ?array {
|
||||
return json_decode( $this->staff_credits ?? "", true );
|
||||
}
|
||||
|
||||
63
app/Models/EntryReview.php
Normal file
63
app/Models/EntryReview.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\HasXenforoUserId;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Str;
|
||||
use League\CommonMark\GithubFlavoredMarkdownConverter;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property int $entry_id
|
||||
* @property string $title
|
||||
* @property int $rating
|
||||
* @property string $description
|
||||
* @property string|null $deleted_at
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\Entry|null $entry
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|EntryReview newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|EntryReview newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|EntryReview query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|EntryReview whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|EntryReview whereDeletedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|EntryReview whereDescription($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|EntryReview whereEntryId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|EntryReview whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|EntryReview whereRating($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|EntryReview whereTitle($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|EntryReview whereUpdatedAt($value)
|
||||
* @property int $user_id
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|EntryReview whereUserId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|EntryReview onlyTrashed()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|EntryReview withTrashed(bool $withTrashed = true)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|EntryReview withoutTrashed()
|
||||
* @property-read string $description_html
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class EntryReview extends Model
|
||||
{
|
||||
|
||||
use HasXenforoUserId, SoftDeletes;
|
||||
|
||||
protected $fillable = [ 'entry_id', 'title', 'rating', 'description', 'user_id' ];
|
||||
|
||||
public function entry(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Entry::class);
|
||||
}
|
||||
|
||||
public function getDescriptionHtmlAttribute(): string
|
||||
{
|
||||
$converter = new GithubFlavoredMarkdownConverter([
|
||||
'html_input' => 'strip',
|
||||
'allow_unsafe_links' => false,
|
||||
]);
|
||||
|
||||
return $converter->convert($this->description)->getContent();
|
||||
}
|
||||
|
||||
}
|
||||
53
app/Models/MigrationUserPlan.php
Normal file
53
app/Models/MigrationUserPlan.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|MigrationUserPlan newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|MigrationUserPlan newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|MigrationUserPlan query()
|
||||
* @property int $id
|
||||
* @property int|null $wp_user_id
|
||||
* @property int|null $xf_user_id
|
||||
* @property string $match_type
|
||||
* @property string|null $email
|
||||
* @property string|null $wp_username
|
||||
* @property string|null $xf_username
|
||||
* @property string|null $note
|
||||
* @property string $status
|
||||
* @property int|null $user_id
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|MigrationUserPlan whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|MigrationUserPlan whereEmail($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|MigrationUserPlan whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|MigrationUserPlan whereMatchType($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|MigrationUserPlan whereNote($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|MigrationUserPlan whereStatus($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|MigrationUserPlan whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|MigrationUserPlan whereUserId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|MigrationUserPlan whereWpUserId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|MigrationUserPlan whereWpUsername($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|MigrationUserPlan whereXfUserId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|MigrationUserPlan whereXfUsername($value)
|
||||
* @property int $wp_password_bridge
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|MigrationUserPlan whereWpPasswordBridge($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class MigrationUserPlan extends Model
|
||||
{
|
||||
protected $table = 'migration_user_plan';
|
||||
|
||||
protected $fillable = [
|
||||
'wp_user_id',
|
||||
'xf_user_id',
|
||||
'match_type',
|
||||
'email',
|
||||
'wp_username',
|
||||
'xf_username',
|
||||
'note',
|
||||
'status',
|
||||
];
|
||||
}
|
||||
@@ -4,10 +4,12 @@ namespace App\Models;
|
||||
|
||||
use App\Helpers\EntryHelpers;
|
||||
use App\Traits\HasGallery;
|
||||
use App\Traits\HasXenforoUserId;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use League\CommonMark\GithubFlavoredMarkdownConverter;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
@@ -59,7 +61,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
class News extends Model
|
||||
{
|
||||
|
||||
use SoftDeletes, HasGallery;
|
||||
use SoftDeletes, HasGallery, HasXenforoUserId;
|
||||
|
||||
protected $table = 'news';
|
||||
|
||||
@@ -108,6 +110,16 @@ class News extends Model
|
||||
return $this->belongsTo(Category::class);
|
||||
}
|
||||
|
||||
public function getDescriptionHtmlAttribute(): string
|
||||
{
|
||||
$converter = new GithubFlavoredMarkdownConverter([
|
||||
'html_input' => 'strip',
|
||||
'allow_unsafe_links' => false,
|
||||
]);
|
||||
|
||||
return $converter->convert($this->description)->getContent();
|
||||
}
|
||||
|
||||
public function getYoutubeVideoId(): ?string {
|
||||
if( !$this->youtube_link )
|
||||
return null;
|
||||
|
||||
36
app/Policies/EntryReviewPolicy.php
Normal file
36
app/Policies/EntryReviewPolicy.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\Entry;
|
||||
use App\Models\EntryReview;
|
||||
use App\Models\User;
|
||||
|
||||
class EntryReviewPolicy
|
||||
{
|
||||
public function viewAny(\App\Auth\XenForoUser $user): bool
|
||||
{
|
||||
if( $user->_can( 'romhackplaza', 'view' ) )
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function create(\App\Auth\XenForoUser $user, ?EntryReview $review = null ): bool
|
||||
{
|
||||
return $user->_can( 'romhackplaza', 'canSubmitEntry' );
|
||||
}
|
||||
|
||||
public function update(\App\Auth\XenForoUser $user, ?EntryReview $review = null ): bool
|
||||
{
|
||||
// Staff editors
|
||||
if( $user->_can('romhackplaza', 'canEditOthersEntries') )
|
||||
return true;
|
||||
|
||||
// Author.
|
||||
if( $user->_can( 'romhackplaza', 'canEditMyEntries' ) && $review->user_id === $user->user_id )
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ namespace App\Providers;
|
||||
use App\Auth\XenForoGuard;
|
||||
use App\Auth\XenForoUser;
|
||||
use App\Policies\TempFilePolicy;
|
||||
use App\Proxy\VisitorProxy;
|
||||
use App\Services\TemporaryFileService;
|
||||
use App\Support\XenForoCauserResolver;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
@@ -39,5 +40,10 @@ class AppServiceProvider extends ServiceProvider
|
||||
Gate::define('is-mod', function (XenForoUser $user) {
|
||||
return $user->is_moderator === 1;
|
||||
});
|
||||
|
||||
\View::composer('*', function ($view) {
|
||||
$view->with('VISITOR', new VisitorProxy( \Auth::user() ) );
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
43
app/Proxy/VisitorProxy.php
Normal file
43
app/Proxy/VisitorProxy.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Proxy;
|
||||
|
||||
use App\Auth\XenForoUser;
|
||||
use App\Services\XenforoService;
|
||||
|
||||
/**
|
||||
* @mixin XenForoUser
|
||||
*/
|
||||
class VisitorProxy
|
||||
{
|
||||
private ?XenForoUser $currentVisitor;
|
||||
private array $users = [];
|
||||
|
||||
public function __construct(?XenForoUser $user)
|
||||
{
|
||||
$this->currentVisitor = $user;
|
||||
}
|
||||
|
||||
public function __get( string $name ): mixed
|
||||
{
|
||||
return $this->currentVisitor?->$name;
|
||||
}
|
||||
|
||||
public function __invoke( int $userId ): ?XenForoUser
|
||||
{
|
||||
if( !isset( $this->users[$userId] ) ){
|
||||
$this->users[$userId] = app(XenforoService::class)->getXfUser($userId);
|
||||
}
|
||||
return $this->users[$userId];
|
||||
}
|
||||
|
||||
public function loggedIn(): bool
|
||||
{
|
||||
return $this->currentVisitor !== null;
|
||||
}
|
||||
|
||||
public function guest(): bool
|
||||
{
|
||||
return $this->currentVisitor === null;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Entry;
|
||||
use App\Models\EntryReview;
|
||||
use App\Models\News;
|
||||
use App\View\Components\EntryCard;
|
||||
use Illuminate\Support\Carbon;
|
||||
@@ -17,9 +18,10 @@ class ActivityService
|
||||
private const CACHE_MESSAGES = 300; // seconds.
|
||||
private const CACHE_THREADS = 300; // seconds.
|
||||
private const CACHE_CLUBS = 300; // seconds.
|
||||
private const CACHE_REVIEWS = 300; // seconds.
|
||||
private const ITEMS_PER_TYPE = 15;
|
||||
|
||||
public function getActivities( array $activities = [ 'entries', 'news', 'messages', 'threads', 'clubs' ] ): Collection
|
||||
public function getActivities( array $activities = [ 'entries', 'news', 'messages', 'threads', 'clubs', 'reviews' ] ): Collection
|
||||
{
|
||||
$c = collect();
|
||||
if( in_array( 'entries', $activities ) ) {
|
||||
@@ -37,6 +39,9 @@ class ActivityService
|
||||
if( in_array( 'clubs', $activities ) ) {
|
||||
$c = $c->merge($this->getClubs());
|
||||
}
|
||||
if( in_array( 'reviews', $activities ) ) {
|
||||
$c = $c->merge($this->getReviews());
|
||||
}
|
||||
|
||||
return $c->sortByDesc('date')
|
||||
->values()
|
||||
@@ -135,6 +140,23 @@ class ActivityService
|
||||
];
|
||||
}
|
||||
|
||||
private function formatReview( EntryReview $review ): array
|
||||
{
|
||||
return [
|
||||
'type' => 'review',
|
||||
'title' => $review->title,
|
||||
'url' => $review->entry()->exists() ? route('entries.show', ['section' => $review->entry->type, 'entry' => $review->entry]) : '',
|
||||
'image' => null,
|
||||
'date' => $review->created_at->timestamp,
|
||||
'author' => null,
|
||||
'user_id' => $review->user_id,
|
||||
'badge' => 'Review',
|
||||
'badge_class' => 'review',
|
||||
'excerpt' => $review->description ? \Str::limit(strip_tags($review->description), 80) : null,
|
||||
'meta' => $review->entry()->exists() ? ( $review->entry->complete_title ?? $review->entry->title ) : null,
|
||||
];
|
||||
}
|
||||
|
||||
private function getEntries(): array
|
||||
{
|
||||
return Cache::remember('activity_entries', self::CACHE_ENTRIES, function() {
|
||||
@@ -220,4 +242,16 @@ class ActivityService
|
||||
->toArray();
|
||||
});
|
||||
}
|
||||
|
||||
private function getReviews(): array
|
||||
{
|
||||
return Cache::remember('activity_reviews', self::CACHE_REVIEWS, function() {
|
||||
return EntryReview::with(['entry'])
|
||||
->latest('created_at')
|
||||
->limit(self::ITEMS_PER_TYPE)
|
||||
->get()
|
||||
->map($this->formatReview(...))
|
||||
->toArray();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
46
app/Services/ReviewsService.php
Normal file
46
app/Services/ReviewsService.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Helpers\EntryHelpers;
|
||||
use App\Models\Entry;
|
||||
use App\Models\EntryReview;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ReviewsService
|
||||
{
|
||||
|
||||
private ?Request $request = null;
|
||||
private ?Entry $entry = null;
|
||||
private ?EntryReview $entryReview = null;
|
||||
|
||||
/**
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function storeReview(Request $request, Entry $entry)
|
||||
{
|
||||
// Step 1: Prepare fields.
|
||||
$this->request = $request;
|
||||
$this->entry = $entry;
|
||||
$user_id = \Auth::user()->user_id;
|
||||
|
||||
$review = DB::transaction(function () use ($user_id) {
|
||||
|
||||
$fields = [
|
||||
'entry_id' => $this->entry->id,
|
||||
'title' => $this->request->input('title'),
|
||||
'rating'=> $this->request->input('rating'),
|
||||
'description' => $this->request->input('description'),
|
||||
'user_id' => $user_id,
|
||||
];
|
||||
|
||||
$review = EntryReview::create($fields);
|
||||
|
||||
return $review;
|
||||
|
||||
});
|
||||
|
||||
return $review;
|
||||
}
|
||||
}
|
||||
@@ -201,7 +201,7 @@ class SubmissionsService {
|
||||
$this->Step11_SaveLanguages( $entry );
|
||||
|
||||
// STEP 11.5 : Save Categories
|
||||
if( section_must_be( 'utilities', $this->section ) ) {
|
||||
if( section_must_be( ['utilities', 'documents'], $this->section ) ) {
|
||||
$this->Step11_5_SaveCategories($entry);
|
||||
}
|
||||
|
||||
@@ -605,6 +605,9 @@ class SubmissionsService {
|
||||
if( $fields['featured'] == false )
|
||||
$fields['featured_at'] = null;
|
||||
$fields['comments_thread_id'] = $this->request->input('comments_thread_id');
|
||||
$refresh_created_at = $this->request->input('refresh_created_at') ?? false;
|
||||
if( $refresh_created_at )
|
||||
$fields['created_at'] = now();
|
||||
}
|
||||
|
||||
$this->entry->update( $fields );
|
||||
@@ -631,7 +634,7 @@ class SubmissionsService {
|
||||
$this->eStep10_UpdateLanguages();
|
||||
|
||||
// STEP 10.5 : Update categories
|
||||
if( section_must_be( 'utilities', $this->section ) )
|
||||
if( section_must_be( ['utilities', 'documents'], $this->section ) )
|
||||
$this->eStep10_5_UpdateCategories();
|
||||
|
||||
// STEP 11: Prepare new gallery images and prepare deletion of others ones.
|
||||
|
||||
@@ -24,7 +24,7 @@ class XenforoApiService {
|
||||
*/
|
||||
private function get(string $endpoint, ?int $customUserId = null ): mixed
|
||||
{
|
||||
$response = Http::withHeaders([
|
||||
$response = Http::timeout(30)->withHeaders([
|
||||
'XF-Api-Key' => $this->apiKey,
|
||||
'XF-Api-User' => $customUserId ?? $this->superUserId,
|
||||
])->get("{$this->apiUrl}/{$endpoint}");
|
||||
@@ -37,7 +37,7 @@ class XenforoApiService {
|
||||
|
||||
private function post(string $endpoint, ?int $customUserId = null, array $data = [] ): mixed
|
||||
{
|
||||
$response = Http::withHeaders([
|
||||
$response = Http::timeout(30)->withHeaders([
|
||||
'XF-Api-Key' => $this->apiKey,
|
||||
'XF-Api-User' => $customUserId ?? $this->superUserId,
|
||||
])->post("{$this->apiUrl}/{$endpoint}", $data);
|
||||
@@ -50,7 +50,7 @@ class XenforoApiService {
|
||||
|
||||
private function delete(string $endpoint, ?int $customUserId = null, array $data = [] ): mixed
|
||||
{
|
||||
$response = Http::withHeaders([
|
||||
$response = Http::timeout(30)->withHeaders([
|
||||
'XF-Api-Key' => $this->apiKey,
|
||||
'XF-Api-User' => $customUserId ?? $this->superUserId,
|
||||
])->delete("{$this->apiUrl}/{$endpoint}", $data);
|
||||
@@ -75,7 +75,7 @@ class XenforoApiService {
|
||||
public function markAllNotificationsRead(int $userId): void
|
||||
{
|
||||
Cache::forget("xf_alerts_{$userId}");
|
||||
$this->post("alerts/marl-all", $userId );
|
||||
$this->post("alerts/mark-all", $userId );
|
||||
}
|
||||
|
||||
public function getConversations(int $userId): mixed
|
||||
@@ -87,8 +87,9 @@ class XenforoApiService {
|
||||
|
||||
public function createConversation( array $userIdList, string $title, string $message, bool $conversationOpen, bool $openInvite ): bool
|
||||
{
|
||||
$response = $this->post("conversations", data: ['recipient_ids' => $userIdList, 'title' => $title, 'message' => $message, 'open_invite' => $openInvite, 'conversation_open' => $conversationOpen] );
|
||||
|
||||
$response = $this->post("conversations",
|
||||
data: ['recipient_ids' => $userIdList, 'title' => $title, 'message' => $message, 'open_invite' => $openInvite, 'conversation_open' => $conversationOpen]
|
||||
);
|
||||
return $response['success'] ?? false;
|
||||
}
|
||||
|
||||
@@ -152,4 +153,13 @@ class XenforoApiService {
|
||||
return (bool) $this->post("threads/{$threadId}/undelete" );
|
||||
}
|
||||
|
||||
public function _migrateUser(array $data): array
|
||||
{
|
||||
$response = $this->post("migrate/user", data: $data );
|
||||
if( !$response || !$response['success'] )
|
||||
return [false,false];
|
||||
|
||||
return [ $response['user_id'] ?? false, $response['password_set'] ?? false ];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
15
app/Traits/HasXenforoUserId.php
Normal file
15
app/Traits/HasXenforoUserId.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Traits;
|
||||
|
||||
use App\Auth\XenForoUser;
|
||||
use App\Services\XenforoService;
|
||||
|
||||
trait HasXenforoUserId
|
||||
{
|
||||
public function xenforoUser(): ?XenForoUser
|
||||
{
|
||||
$service = app(XenforoService::class);
|
||||
return $service->getXfUser( $this->user_id );
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ class DatabaseFilterWithModeSearch extends Component
|
||||
public string $model,
|
||||
public string $modeModel,
|
||||
public string $selectedMode,
|
||||
|
||||
public string $searchModel,
|
||||
public string $idProperty = 'id',
|
||||
public string $nameProperty = 'name',
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@ class DatabaseFilterWithoutModeSearch extends Component
|
||||
public string $title,
|
||||
public $items,
|
||||
public string $model,
|
||||
|
||||
public string $searchModel,
|
||||
public string $idProperty = 'id',
|
||||
public string $nameProperty = 'name',
|
||||
)
|
||||
|
||||
@@ -17,6 +17,10 @@ class ErrorBlock extends Component
|
||||
'page-not-allowed' => [
|
||||
'icon' => 'shield-ban',
|
||||
'message' => "You do not have permission to access this page.\nRequired permission: %s"
|
||||
],
|
||||
'user-state-not-valid' => [
|
||||
'icon' => 'shield-ban',
|
||||
'message' => "You do not have permission to access this page.\nYour user profile is incomplete: %s\nGo back to the forum for more details."
|
||||
]
|
||||
];
|
||||
|
||||
|
||||
30
app/View/Components/ReviewCard.php
Normal file
30
app/View/Components/ReviewCard.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\View\Components;
|
||||
|
||||
use App\Models\EntryReview;
|
||||
use Closure;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class ReviewCard extends Component
|
||||
{
|
||||
/**
|
||||
* Create a new component instance.
|
||||
*/
|
||||
public function __construct(
|
||||
public EntryReview $review,
|
||||
public bool $entryShow = false
|
||||
)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the view / contents that represent the component.
|
||||
*/
|
||||
public function render(): View|Closure|string
|
||||
{
|
||||
return view('components.review-card');
|
||||
}
|
||||
}
|
||||
28
app/View/Components/ReviewStarRating.php
Normal file
28
app/View/Components/ReviewStarRating.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\View\Components;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class ReviewStarRating extends Component
|
||||
{
|
||||
/**
|
||||
* Create a new component instance.
|
||||
*/
|
||||
public function __construct(
|
||||
public float $rating,
|
||||
)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the view / contents that represent the component.
|
||||
*/
|
||||
public function render(): View|Closure|string
|
||||
{
|
||||
return view('components.review-star-rating');
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
use League\HTMLToMarkdown\HtmlConverter;
|
||||
|
||||
/* SECTIONS HELPERS */
|
||||
if( !function_exists( 'section_must_be' ) ){
|
||||
|
||||
@@ -34,9 +36,9 @@ if( !function_exists('userTheme' ) ){
|
||||
}
|
||||
}
|
||||
|
||||
if( !function_exists( 'databaseRoute' ) ){
|
||||
if( !function_exists( 'databaseRoute' ) ) {
|
||||
|
||||
function databaseRoute( array $params = [] ): string
|
||||
function databaseRoute(array $params = []): string
|
||||
{
|
||||
$defaults = [
|
||||
'types' => [],
|
||||
@@ -50,6 +52,11 @@ if( !function_exists( 'databaseRoute' ) ){
|
||||
'languagesMode' => 'or',
|
||||
'modifications' => [],
|
||||
'modificationsMode' => 'or',
|
||||
'categories' => [],
|
||||
'categoriesMode' => 'or',
|
||||
'systems' => [],
|
||||
'systemsMode' => 'or',
|
||||
'levels' => [],
|
||||
'sort' => 'created_at',
|
||||
'dir' => 'desc',
|
||||
's' => ''
|
||||
@@ -57,9 +64,9 @@ if( !function_exists( 'databaseRoute' ) ){
|
||||
|
||||
$query = array_filter(
|
||||
array_merge($defaults, $params),
|
||||
fn($v,$k) => match(true){
|
||||
fn($v, $k) => match (true) {
|
||||
is_array($v) => !empty($v),
|
||||
in_array($k, ['authorsMode', 'languagesMode', 'modificationsMode']) => $v !== 'or',
|
||||
in_array($k, ['authorsMode', 'languagesMode', 'modificationsMode', 'categoriesMode', 'systemsMode']) => $v !== 'or',
|
||||
$k === 'sort' => $v !== 'created_at',
|
||||
$k === 'dir' => $v !== 'desc',
|
||||
default => $v !== '',
|
||||
@@ -67,6 +74,57 @@ if( !function_exists( 'databaseRoute' ) ){
|
||||
ARRAY_FILTER_USE_BOTH
|
||||
);
|
||||
|
||||
return route('entries.index', $query );
|
||||
return route('entries.index', $query);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if( !function_exists( 'newsRoute' ) ){
|
||||
function newsRoute(array $params = []): string
|
||||
{
|
||||
$defaults = [
|
||||
'categories' => [],
|
||||
'sort' => 'created_at',
|
||||
'dir' => 'desc',
|
||||
's' => ''
|
||||
];
|
||||
|
||||
$query = array_filter(
|
||||
array_merge($defaults, $params),
|
||||
fn($v, $k) => match (true) {
|
||||
is_array($v) => !empty($v),
|
||||
$k === 'sort' => $v !== 'created_at',
|
||||
$k === 'dir' => $v !== 'desc',
|
||||
default => $v !== '',
|
||||
},
|
||||
ARRAY_FILTER_USE_BOTH
|
||||
);
|
||||
|
||||
return route('news.index', $query);
|
||||
}
|
||||
}
|
||||
|
||||
if( !function_exists('reviewsRoute') ){
|
||||
function reviewsRoute( array $params = [] ): string
|
||||
{
|
||||
$defaults = [
|
||||
'entryId' => null,
|
||||
'rating' => null,
|
||||
'sort' => 'created_at',
|
||||
'dir' => 'desc',
|
||||
];
|
||||
|
||||
$query = array_filter(
|
||||
array_merge($defaults, $params),
|
||||
fn($v,$k) => match(true){
|
||||
is_array($v) => !empty($v),
|
||||
$k === 'sort' => $v !== 'created_at',
|
||||
$k === 'dir' => $v !== 'desc',
|
||||
default => $v !== '',
|
||||
},
|
||||
ARRAY_FILTER_USE_BOTH
|
||||
);
|
||||
|
||||
return route('reviews.index', $query );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
if( $request->is('manage*'))
|
||||
abort(403);
|
||||
});
|
||||
$middleware->append(\App\Http\Middleware\CheckXenForoUserState::class);
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions): void {
|
||||
//
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
"filament/filament": "^5.6",
|
||||
"laravel/framework": "^13.7",
|
||||
"laravel/tinker": "^3.0",
|
||||
"league/commonmark": "^2.8",
|
||||
"league/html-to-markdown": "^5.1",
|
||||
"livewire/livewire": "^4.3",
|
||||
"predis/predis": "^3.4",
|
||||
"spatie/laravel-activitylog": "^5.0"
|
||||
|
||||
91
composer.lock
generated
91
composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "d561062afd8c291a93e8fd1e00f4e901",
|
||||
"content-hash": "ea9258b1759d46665d2487b066c686ee",
|
||||
"packages": [
|
||||
{
|
||||
"name": "blade-ui-kit/blade-heroicons",
|
||||
@@ -2967,6 +2967,95 @@
|
||||
},
|
||||
"time": "2026-01-23T15:30:45+00:00"
|
||||
},
|
||||
{
|
||||
"name": "league/html-to-markdown",
|
||||
"version": "5.1.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/thephpleague/html-to-markdown.git",
|
||||
"reference": "0b4066eede55c48f38bcee4fb8f0aa85654390fd"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/thephpleague/html-to-markdown/zipball/0b4066eede55c48f38bcee4fb8f0aa85654390fd",
|
||||
"reference": "0b4066eede55c48f38bcee4fb8f0aa85654390fd",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-dom": "*",
|
||||
"ext-xml": "*",
|
||||
"php": "^7.2.5 || ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"mikehaertl/php-shellcommand": "^1.1.0",
|
||||
"phpstan/phpstan": "^1.8.8",
|
||||
"phpunit/phpunit": "^8.5 || ^9.2",
|
||||
"scrutinizer/ocular": "^1.6",
|
||||
"unleashedtech/php-coding-standard": "^2.7 || ^3.0",
|
||||
"vimeo/psalm": "^4.22 || ^5.0"
|
||||
},
|
||||
"bin": [
|
||||
"bin/html-to-markdown"
|
||||
],
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "5.2-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"League\\HTMLToMarkdown\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Colin O'Dell",
|
||||
"email": "colinodell@gmail.com",
|
||||
"homepage": "https://www.colinodell.com",
|
||||
"role": "Lead Developer"
|
||||
},
|
||||
{
|
||||
"name": "Nick Cernis",
|
||||
"email": "nick@cern.is",
|
||||
"homepage": "http://modernnerd.net",
|
||||
"role": "Original Author"
|
||||
}
|
||||
],
|
||||
"description": "An HTML-to-markdown conversion helper for PHP",
|
||||
"homepage": "https://github.com/thephpleague/html-to-markdown",
|
||||
"keywords": [
|
||||
"html",
|
||||
"markdown"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/thephpleague/html-to-markdown/issues",
|
||||
"source": "https://github.com/thephpleague/html-to-markdown/tree/5.1.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://www.colinodell.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://www.paypal.me/colinpodell/10.00",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/colinodell",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/league/html-to-markdown",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2023-07-12T21:21:09+00:00"
|
||||
},
|
||||
{
|
||||
"name": "league/mime-type-detection",
|
||||
"version": "1.16.0",
|
||||
|
||||
@@ -136,6 +136,32 @@ return [
|
||||
'database' => env('DISCORD_DB_PATH'),
|
||||
'prefix' => '',
|
||||
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
|
||||
],
|
||||
|
||||
// Migration.
|
||||
|
||||
'old_wp' => [
|
||||
'driver' => 'mysql',
|
||||
'host' => env('MIG_WP_DB_HOST'),
|
||||
'port' => env('MIG_WP_DB_PORT', '3306'),
|
||||
'database' => env('MIG_WP_DB_NAME'),
|
||||
'username' => env('MIG_WP_DB_USERNAME'),
|
||||
'password' => env('MIG_WP_DB_PASSWORD'),
|
||||
'charset' => env('MIG_WP_DB_CHARSET', 'utf8mb4'),
|
||||
'prefix' => 'wp_',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
],
|
||||
|
||||
'old_xf' => [
|
||||
'driver' => 'mysql',
|
||||
'host' => env('MIG_XF_DB_HOST'),
|
||||
'port' => env('MIG_XF_DB_PORT', '3306'),
|
||||
'database' => env('MIG_XF_DB_NAME'),
|
||||
'username' => env('MIG_XF_DB_USERNAME'),
|
||||
'password' => env('MIG_XF_DB_PASSWORD'),
|
||||
'charset' => env('MIG_XF_DB_CHARSET', 'utf8mb4'),
|
||||
'prefix' => 'xf_',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
]
|
||||
|
||||
],
|
||||
|
||||
@@ -73,12 +73,7 @@ return [
|
||||
[
|
||||
'name' => 'ROM Hasher',
|
||||
'icon' => 'hash',
|
||||
'route' => 'home'
|
||||
],
|
||||
[
|
||||
'name' => 'ROM Checker',
|
||||
'icon' => 'check',
|
||||
'route' => 'home'
|
||||
'route' => 'tools.hash'
|
||||
]
|
||||
]
|
||||
],
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('entry_reviews', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('entry_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('title', 255);
|
||||
$table->integer('rating');
|
||||
$table->text('description');
|
||||
$table->unsignedBigInteger( 'user_id' ); // xf_user_id
|
||||
$table->softDeletes();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('entry_reviews');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('migration_user_plan', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('wp_user_id')->nullable();
|
||||
$table->unsignedBigInteger('xf_user_id')->nullable();
|
||||
$table->string('match_type');
|
||||
$table->string('email')->nullable();
|
||||
$table->string('wp_username')->nullable();
|
||||
$table->string('xf_username')->nullable();
|
||||
$table->text('note')->nullable();
|
||||
$table->string('status')->default('pending');
|
||||
$table->unsignedBigInteger('user_id')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('match_type');
|
||||
$table->index('status');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('migration_user_plan');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('migration_settings', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->json('value');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('migration_settings');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('migrations_logs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('source_system');
|
||||
$table->string('source_table');
|
||||
$table->unsignedBigInteger('source_id');
|
||||
$table->string('target_table');
|
||||
$table->unsignedBigInteger('target_id');
|
||||
$table->string('status')->default('pending');
|
||||
$table->dateTime('migrated_at')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('migrations_logs');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('migration_user_plan', function (Blueprint $table) {
|
||||
$table->boolean('wp_password_bridge')->default(false);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('migration_user_plan', function (Blueprint $table) {
|
||||
$table->dropColumn('wp_password_bridge');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('migration_game_plan', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('wp_game_id');
|
||||
$table->unsignedBigInteger('wp_platform_id');
|
||||
$table->unsignedBigInteger('game_id')->nullable();
|
||||
$table->unsignedBigInteger('wp_genre_id')->nullable();
|
||||
$table->unsignedInteger('post_count')->default(0);
|
||||
$table->boolean('genre_conflict')->default(false);
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['wp_game_id', 'wp_platform_id']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('migration_game_plan');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('galleries', function (Blueprint $table) {
|
||||
$table->dropForeign('entry_galleries_entry_id_foreign');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
};
|
||||
634
database/schema/mariadb-schema.sql
Normal file
634
database/schema/mariadb-schema.sql
Normal file
@@ -0,0 +1,634 @@
|
||||
/*M!999999\- enable the sandbox mode */
|
||||
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
|
||||
/*!40103 SET TIME_ZONE='+00:00' */;
|
||||
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
|
||||
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
|
||||
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
|
||||
/*M!100616 SET @OLD_NOTE_VERBOSITY=@@NOTE_VERBOSITY, NOTE_VERBOSITY=0 */;
|
||||
DROP TABLE IF EXISTS `activity_log`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `activity_log` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`log_name` varchar(255) DEFAULT NULL,
|
||||
`description` text NOT NULL,
|
||||
`subject_type` varchar(255) DEFAULT NULL,
|
||||
`subject_id` bigint(20) unsigned DEFAULT NULL,
|
||||
`event` varchar(255) DEFAULT NULL,
|
||||
`causer_type` varchar(255) DEFAULT NULL,
|
||||
`causer_id` bigint(20) unsigned DEFAULT NULL,
|
||||
`attribute_changes` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`attribute_changes`)),
|
||||
`properties` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`properties`)),
|
||||
`created_at` timestamp NULL DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `subject` (`subject_type`,`subject_id`),
|
||||
KEY `causer` (`causer_type`,`causer_id`),
|
||||
KEY `activity_log_log_name_index` (`log_name`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `authors`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `authors` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`slug` varchar(255) NOT NULL,
|
||||
`website` varchar(500) DEFAULT NULL,
|
||||
`user_id` int(10) unsigned DEFAULT NULL,
|
||||
`created_at` timestamp NULL DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `authors_slug_unique` (`slug`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `cache`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `cache` (
|
||||
`key` varchar(255) NOT NULL,
|
||||
`value` mediumtext NOT NULL,
|
||||
`expiration` bigint(20) NOT NULL,
|
||||
PRIMARY KEY (`key`),
|
||||
KEY `cache_expiration_index` (`expiration`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `cache_locks`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `cache_locks` (
|
||||
`key` varchar(255) NOT NULL,
|
||||
`owner` varchar(255) NOT NULL,
|
||||
`expiration` bigint(20) NOT NULL,
|
||||
PRIMARY KEY (`key`),
|
||||
KEY `cache_locks_expiration_index` (`expiration`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `categories`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `categories` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`slug` varchar(255) NOT NULL,
|
||||
`created_at` timestamp NULL DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL,
|
||||
`restricted_to` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`restricted_to`)),
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `categories_slug_unique` (`slug`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `entries`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `entries` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`type` enum('translations','romhacks','homebrew','utilities','documents','lua-scripts','tutorials') NOT NULL,
|
||||
`title` varchar(255) DEFAULT NULL,
|
||||
`slug` varchar(255) DEFAULT NULL,
|
||||
`description` longtext DEFAULT NULL,
|
||||
`main_image` varchar(255) DEFAULT NULL,
|
||||
`state` enum('draft','pending','published','locked','rejected','hidden') NOT NULL DEFAULT 'draft',
|
||||
`staff_comment` text DEFAULT NULL,
|
||||
`rejected_at` timestamp NULL DEFAULT NULL,
|
||||
`featured` tinyint(1) NOT NULL DEFAULT 0,
|
||||
`featured_at` datetime DEFAULT NULL,
|
||||
`game_id` bigint(20) unsigned DEFAULT NULL,
|
||||
`platform_id` bigint(20) unsigned DEFAULT NULL,
|
||||
`status_id` bigint(20) unsigned DEFAULT NULL,
|
||||
`version` varchar(50) DEFAULT NULL,
|
||||
`release_date` date DEFAULT NULL,
|
||||
`staff_credits` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`staff_credits`)),
|
||||
`relevant_link` varchar(500) DEFAULT NULL,
|
||||
`youtube_link` varchar(500) DEFAULT NULL,
|
||||
`user_id` bigint(20) unsigned NOT NULL,
|
||||
`comments_thread_id` bigint(20) unsigned DEFAULT NULL,
|
||||
`created_at` timestamp NULL DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL,
|
||||
`complete_title` varchar(255) DEFAULT NULL,
|
||||
`deleted_at` timestamp NULL DEFAULT NULL,
|
||||
`level_id` bigint(20) unsigned DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `entries_slug_unique` (`slug`),
|
||||
KEY `entries_game_id_foreign` (`game_id`),
|
||||
KEY `entries_platform_id_foreign` (`platform_id`),
|
||||
KEY `entries_status_id_foreign` (`status_id`),
|
||||
KEY `entries_type_state_game_id_platform_id_status_id_index` (`type`,`state`,`game_id`,`platform_id`,`status_id`),
|
||||
KEY `entries_level_id_foreign` (`level_id`),
|
||||
CONSTRAINT `entries_game_id_foreign` FOREIGN KEY (`game_id`) REFERENCES `games` (`id`) ON DELETE SET NULL,
|
||||
CONSTRAINT `entries_level_id_foreign` FOREIGN KEY (`level_id`) REFERENCES `levels` (`id`) ON DELETE SET NULL,
|
||||
CONSTRAINT `entries_platform_id_foreign` FOREIGN KEY (`platform_id`) REFERENCES `platforms` (`id`) ON DELETE SET NULL,
|
||||
CONSTRAINT `entries_status_id_foreign` FOREIGN KEY (`status_id`) REFERENCES `statuses` (`id`) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `entry_authors`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `entry_authors` (
|
||||
`entry_id` bigint(20) unsigned NOT NULL,
|
||||
`author_id` bigint(20) unsigned NOT NULL,
|
||||
PRIMARY KEY (`entry_id`,`author_id`),
|
||||
KEY `entry_authors_author_id_foreign` (`author_id`),
|
||||
CONSTRAINT `entry_authors_author_id_foreign` FOREIGN KEY (`author_id`) REFERENCES `authors` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `entry_authors_entry_id_foreign` FOREIGN KEY (`entry_id`) REFERENCES `entries` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `entry_categories`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `entry_categories` (
|
||||
`entry_id` bigint(20) unsigned NOT NULL,
|
||||
`category_id` bigint(20) unsigned NOT NULL,
|
||||
PRIMARY KEY (`entry_id`,`category_id`),
|
||||
KEY `entry_categories_category_id_foreign` (`category_id`),
|
||||
CONSTRAINT `entry_categories_category_id_foreign` FOREIGN KEY (`category_id`) REFERENCES `categories` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `entry_categories_entry_id_foreign` FOREIGN KEY (`entry_id`) REFERENCES `entries` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `entry_files`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `entry_files` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`entry_id` bigint(20) unsigned NOT NULL,
|
||||
`filename` varchar(1024) NOT NULL,
|
||||
`filepath` varchar(1024) NOT NULL,
|
||||
`favorite_server` varchar(11) NOT NULL,
|
||||
`favorite_at` timestamp NOT NULL,
|
||||
`filesize` bigint(20) unsigned DEFAULT NULL,
|
||||
`created_at` timestamp NULL DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL,
|
||||
`file_uuid` uuid NOT NULL,
|
||||
`state` enum('public','private','archived') NOT NULL DEFAULT 'public',
|
||||
`online_patcher` tinyint(1) NOT NULL DEFAULT 0,
|
||||
`secondary_online_patcher` tinyint(1) NOT NULL DEFAULT 0,
|
||||
`download_count` bigint(20) unsigned NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `entry_files_file_uuid_unique` (`file_uuid`),
|
||||
KEY `entry_files_entry_id_foreign` (`entry_id`),
|
||||
CONSTRAINT `entry_files_entry_id_foreign` FOREIGN KEY (`entry_id`) REFERENCES `entries` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `entry_hashes`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `entry_hashes` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`entry_id` bigint(20) unsigned NOT NULL,
|
||||
`filename` varchar(256) NOT NULL,
|
||||
`hash_crc32` varchar(256) NOT NULL,
|
||||
`hash_sha1` varchar(256) NOT NULL,
|
||||
`verified` varchar(256) NOT NULL,
|
||||
`created_at` timestamp NULL DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `entry_hashes_entry_id_foreign` (`entry_id`),
|
||||
CONSTRAINT `entry_hashes_entry_id_foreign` FOREIGN KEY (`entry_id`) REFERENCES `entries` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `entry_languages`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `entry_languages` (
|
||||
`entry_id` bigint(20) unsigned NOT NULL,
|
||||
`language_id` bigint(20) unsigned NOT NULL,
|
||||
PRIMARY KEY (`entry_id`,`language_id`),
|
||||
KEY `entry_languages_language_id_foreign` (`language_id`),
|
||||
CONSTRAINT `entry_languages_entry_id_foreign` FOREIGN KEY (`entry_id`) REFERENCES `entries` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `entry_languages_language_id_foreign` FOREIGN KEY (`language_id`) REFERENCES `languages` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `entry_modifications`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `entry_modifications` (
|
||||
`entry_id` bigint(20) unsigned NOT NULL,
|
||||
`modification_id` bigint(20) unsigned NOT NULL,
|
||||
PRIMARY KEY (`entry_id`,`modification_id`),
|
||||
KEY `entry_modifications_modification_id_foreign` (`modification_id`),
|
||||
CONSTRAINT `entry_modifications_entry_id_foreign` FOREIGN KEY (`entry_id`) REFERENCES `entries` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `entry_modifications_modification_id_foreign` FOREIGN KEY (`modification_id`) REFERENCES `modifications` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `entry_reviews`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `entry_reviews` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`entry_id` bigint(20) unsigned NOT NULL,
|
||||
`title` varchar(255) NOT NULL,
|
||||
`rating` int(11) NOT NULL,
|
||||
`description` text NOT NULL,
|
||||
`user_id` bigint(20) unsigned NOT NULL,
|
||||
`deleted_at` timestamp NULL DEFAULT NULL,
|
||||
`created_at` timestamp NULL DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `entry_reviews_entry_id_foreign` (`entry_id`),
|
||||
CONSTRAINT `entry_reviews_entry_id_foreign` FOREIGN KEY (`entry_id`) REFERENCES `entries` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `entry_systems`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `entry_systems` (
|
||||
`entry_id` bigint(20) unsigned NOT NULL,
|
||||
`system_id` bigint(20) unsigned NOT NULL,
|
||||
PRIMARY KEY (`entry_id`,`system_id`),
|
||||
KEY `entry_systems_system_id_foreign` (`system_id`),
|
||||
CONSTRAINT `entry_systems_entry_id_foreign` FOREIGN KEY (`entry_id`) REFERENCES `entries` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `entry_systems_system_id_foreign` FOREIGN KEY (`system_id`) REFERENCES `systems` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `failed_jobs`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `failed_jobs` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`uuid` varchar(255) NOT NULL,
|
||||
`connection` text NOT NULL,
|
||||
`queue` text NOT NULL,
|
||||
`payload` longtext NOT NULL,
|
||||
`exception` longtext NOT NULL,
|
||||
`failed_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `failed_jobs_uuid_unique` (`uuid`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `galleries`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `galleries` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`galleryable_type` varchar(255) NOT NULL DEFAULT 'AppModelsEntry',
|
||||
`galleryable_id` bigint(20) unsigned NOT NULL,
|
||||
`image` varchar(255) NOT NULL,
|
||||
`order` smallint(5) unsigned NOT NULL DEFAULT 0,
|
||||
`created_at` timestamp NULL DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `entry_galleries_entry_id_foreign` (`galleryable_id`),
|
||||
KEY `galleries_galleryable_type_galleryable_id_index` (`galleryable_type`,`galleryable_id`),
|
||||
CONSTRAINT `entry_galleries_entry_id_foreign` FOREIGN KEY (`galleryable_id`) REFERENCES `entries` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `games`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `games` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`slug` varchar(255) NOT NULL,
|
||||
`platform_id` bigint(20) unsigned NOT NULL,
|
||||
`genre_id` bigint(20) unsigned NOT NULL,
|
||||
`created_at` timestamp NULL DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `games_slug_unique` (`slug`),
|
||||
KEY `games_platform_id_foreign` (`platform_id`),
|
||||
KEY `games_genre_id_foreign` (`genre_id`),
|
||||
CONSTRAINT `games_genre_id_foreign` FOREIGN KEY (`genre_id`) REFERENCES `genres` (`id`),
|
||||
CONSTRAINT `games_platform_id_foreign` FOREIGN KEY (`platform_id`) REFERENCES `platforms` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `genres`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `genres` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`slug` varchar(255) NOT NULL,
|
||||
`created_at` timestamp NULL DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `genres_slug_unique` (`slug`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `job_batches`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `job_batches` (
|
||||
`id` varchar(255) NOT NULL,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`total_jobs` int(11) NOT NULL,
|
||||
`pending_jobs` int(11) NOT NULL,
|
||||
`failed_jobs` int(11) NOT NULL,
|
||||
`failed_job_ids` longtext NOT NULL,
|
||||
`options` mediumtext DEFAULT NULL,
|
||||
`cancelled_at` int(11) DEFAULT NULL,
|
||||
`created_at` int(11) NOT NULL,
|
||||
`finished_at` int(11) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `jobs`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `jobs` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`queue` varchar(255) NOT NULL,
|
||||
`payload` longtext NOT NULL,
|
||||
`attempts` smallint(5) unsigned NOT NULL,
|
||||
`reserved_at` int(10) unsigned DEFAULT NULL,
|
||||
`available_at` int(10) unsigned NOT NULL,
|
||||
`created_at` int(10) unsigned NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `jobs_queue_index` (`queue`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `languages`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `languages` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`slug` varchar(255) NOT NULL,
|
||||
`created_at` timestamp NULL DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `languages_slug_unique` (`slug`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `levels`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `levels` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`slug` varchar(255) NOT NULL,
|
||||
`created_at` timestamp NULL DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `levels_slug_unique` (`slug`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `migration_game_plan`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `migration_game_plan` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`wp_game_id` bigint(20) unsigned NOT NULL,
|
||||
`wp_platform_id` bigint(20) unsigned NOT NULL,
|
||||
`game_id` bigint(20) unsigned DEFAULT NULL,
|
||||
`wp_genre_id` bigint(20) unsigned DEFAULT NULL,
|
||||
`post_count` int(10) unsigned NOT NULL DEFAULT 0,
|
||||
`genre_conflict` tinyint(1) NOT NULL DEFAULT 0,
|
||||
`created_at` timestamp NULL DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `migration_game_plan_wp_game_id_wp_platform_id_unique` (`wp_game_id`,`wp_platform_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `migration_settings`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `migration_settings` (
|
||||
`key` varchar(255) NOT NULL,
|
||||
`value` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL CHECK (json_valid(`value`)),
|
||||
`created_at` timestamp NULL DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `migration_user_plan`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `migration_user_plan` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`wp_user_id` bigint(20) unsigned DEFAULT NULL,
|
||||
`xf_user_id` bigint(20) unsigned DEFAULT NULL,
|
||||
`match_type` varchar(255) NOT NULL,
|
||||
`email` varchar(255) DEFAULT NULL,
|
||||
`wp_username` varchar(255) DEFAULT NULL,
|
||||
`xf_username` varchar(255) DEFAULT NULL,
|
||||
`note` text DEFAULT NULL,
|
||||
`status` varchar(255) NOT NULL DEFAULT 'pending',
|
||||
`user_id` bigint(20) unsigned DEFAULT NULL,
|
||||
`created_at` timestamp NULL DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL,
|
||||
`wp_password_bridge` tinyint(1) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `migration_user_plan_match_type_index` (`match_type`),
|
||||
KEY `migration_user_plan_status_index` (`status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `migrations`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `migrations` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`migration` varchar(255) NOT NULL,
|
||||
`batch` int(11) NOT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `migrations_logs`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `migrations_logs` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`source_system` varchar(255) NOT NULL,
|
||||
`source_table` varchar(255) NOT NULL,
|
||||
`source_id` bigint(20) unsigned NOT NULL,
|
||||
`target_table` varchar(255) NOT NULL,
|
||||
`target_id` bigint(20) unsigned NOT NULL,
|
||||
`status` varchar(255) NOT NULL DEFAULT 'pending',
|
||||
`migrated_at` datetime DEFAULT NULL,
|
||||
`created_at` timestamp NULL DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `modifications`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `modifications` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`slug` varchar(255) NOT NULL,
|
||||
`created_at` timestamp NULL DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `news`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `news` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`title` varchar(255) NOT NULL,
|
||||
`slug` varchar(255) NOT NULL,
|
||||
`category_id` bigint(20) unsigned DEFAULT NULL,
|
||||
`description` longtext NOT NULL,
|
||||
`state` enum('draft','pending','published','locked','rejected','hidden') NOT NULL DEFAULT 'draft',
|
||||
`staff_comment` text DEFAULT NULL,
|
||||
`rejected_at` timestamp NULL DEFAULT NULL,
|
||||
`entry_id` bigint(20) unsigned DEFAULT NULL,
|
||||
`relevant_link` varchar(500) DEFAULT NULL,
|
||||
`youtube_link` varchar(500) DEFAULT NULL,
|
||||
`user_id` bigint(20) unsigned NOT NULL,
|
||||
`comments_thread_id` bigint(20) unsigned DEFAULT NULL,
|
||||
`deleted_at` timestamp NULL DEFAULT NULL,
|
||||
`created_at` timestamp NULL DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `news_slug_unique` (`slug`),
|
||||
KEY `news_category_id_foreign` (`category_id`),
|
||||
KEY `news_entry_id_foreign` (`entry_id`),
|
||||
CONSTRAINT `news_category_id_foreign` FOREIGN KEY (`category_id`) REFERENCES `categories` (`id`) ON DELETE SET NULL,
|
||||
CONSTRAINT `news_entry_id_foreign` FOREIGN KEY (`entry_id`) REFERENCES `entries` (`id`) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `password_reset_tokens`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `password_reset_tokens` (
|
||||
`email` varchar(255) NOT NULL,
|
||||
`token` varchar(255) NOT NULL,
|
||||
`created_at` timestamp NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`email`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `platforms`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `platforms` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(100) NOT NULL,
|
||||
`slug` varchar(100) NOT NULL,
|
||||
`short_name` varchar(30) DEFAULT NULL,
|
||||
`created_at` timestamp NULL DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL,
|
||||
`play_online_core` varchar(255) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `platforms_slug_unique` (`slug`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `play_online_settings`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `play_online_settings` (
|
||||
`file_id` bigint(20) unsigned NOT NULL,
|
||||
`core` varchar(30) NOT NULL,
|
||||
`threads` tinyint(1) NOT NULL DEFAULT 0,
|
||||
`created_at` timestamp NULL DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`file_id`),
|
||||
CONSTRAINT `play_online_settings_file_id_foreign` FOREIGN KEY (`file_id`) REFERENCES `entry_files` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `sessions`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `sessions` (
|
||||
`id` varchar(255) NOT NULL,
|
||||
`user_id` bigint(20) unsigned DEFAULT NULL,
|
||||
`ip_address` varchar(45) DEFAULT NULL,
|
||||
`user_agent` text DEFAULT NULL,
|
||||
`payload` longtext NOT NULL,
|
||||
`last_activity` int(11) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `sessions_user_id_index` (`user_id`),
|
||||
KEY `sessions_last_activity_index` (`last_activity`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `statuses`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `statuses` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`slug` varchar(255) NOT NULL,
|
||||
`created_at` timestamp NULL DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `status_slug_unique` (`slug`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `systems`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `systems` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`slug` varchar(255) NOT NULL,
|
||||
`created_at` timestamp NULL DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `systems_slug_unique` (`slug`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `users`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `users` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`email` varchar(255) NOT NULL,
|
||||
`email_verified_at` timestamp NULL DEFAULT NULL,
|
||||
`password` varchar(255) NOT NULL,
|
||||
`remember_token` varchar(100) DEFAULT NULL,
|
||||
`created_at` timestamp NULL DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `users_email_unique` (`email`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
|
||||
|
||||
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
|
||||
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
|
||||
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
|
||||
/*M!100616 SET NOTE_VERBOSITY=@OLD_NOTE_VERBOSITY */;
|
||||
|
||||
/*M!999999\- enable the sandbox mode */
|
||||
SET @OLD_AUTOCOMMIT=@@AUTOCOMMIT, @@AUTOCOMMIT=0;
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (1,'0001_01_01_000000_create_users_table',1);
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (2,'0001_01_01_000001_create_cache_table',1);
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (3,'0001_01_01_000002_create_jobs_table',1);
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (4,'2026_05_09_153326_create_platforms_table',2);
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (5,'2026_05_09_153901_add_platform_timestamp',3);
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (6,'2026_05_10_071323_create_genres_table',4);
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (7,'2026_05_10_071400_create_games_table',5);
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (8,'2026_05_10_072139_create_languages_table',6);
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (9,'2026_05_10_072201_create_authors_table',6);
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (10,'2026_05_10_072332_create_modifications_table',6);
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (11,'2026_05_10_072441_create_status_table',6);
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (12,'2026_05_10_072747_create_entries_table',7);
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (13,'2026_05_10_074735_create_entry_authors_table',8);
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (14,'2026_05_10_074830_create_entry_languages_table',8);
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (15,'2026_05_10_074907_create_entry_modifications_table',8);
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (16,'2026_05_12_114815_create_entry_files_table',9);
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (19,'2026_05_12_115134_create_entry_hashes_table',10);
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (20,'2026_05_13_084522_add_fields_to_entry_files',11);
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (22,'2026_05_16_134002_create_entry_gallery_table',12);
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (23,'2026_05_18_131712_change_staff_credits_field',13);
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (24,'2026_05_19_090838_add_complete_title_to_entry',14);
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (25,'2026_05_27_192635_add_fields_for_queue_to_entries',15);
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (26,'2026_05_27_193235_add_rejected_state_to_entries',16);
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (27,'2026_06_04_083346_make_entries_fields_draft_compatible',17);
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (35,'2026_06_05_163235_add_online_patcher_fields_to_files',18);
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (36,'2026_06_09_122425_create_category_table',18);
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (37,'2026_06_09_122655_create_os_table',18);
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (38,'2026_06_09_122817_create_level_table',18);
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (39,'2026_06_09_123242_add_entry_level_id_field',18);
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (40,'2026_06_09_123458_create_entry_systems_table',18);
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (41,'2026_06_09_123533_create_entry_categories_table',18);
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (42,'2026_06_09_124832_add_restricted_field_to_categories',19);
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (43,'2026_06_10_084936_make_entry_galleries_polymorphic',20);
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (44,'2026_06_10_090320_create_news_table',21);
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (45,'2026_06_10_091105_add_fields_for_queue_to_news',22);
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (46,'2026_06_11_155053_add_order_to_galleries_table',23);
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (47,'2026_06_13_210606_add_featured_at_field_to_entries',24);
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (48,'2026_06_14_163648_create_play_online_settings_table',25);
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (49,'2026_06_14_174906_add_default_core_play_online_for_platforms',26);
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (50,'2026_06_16_100941_create_activity_log_table',27);
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (51,'2026_06_16_122812_add_download_field_to_entry_files',28);
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (53,'2026_06_17_114641_create_reviews_table',29);
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (54,'2026_06_18_084700_create_migration_user_plan_table',30);
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (55,'2026_06_19_080416_create_migration_settings_table',31);
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (56,'2026_06_19_125237_create_migrations_logs_table',32);
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (57,'2026_06_19_134939_alter_migration_user_plan_table',33);
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (59,'2026_06_21_085615_create_migration_game_plan_table',34);
|
||||
COMMIT;
|
||||
SET AUTOCOMMIT=@OLD_AUTOCOMMIT;
|
||||
1853
extra.less
1853
extra.less
File diff suppressed because it is too large
Load Diff
BIN
public/ZELDA.ips
BIN
public/ZELDA.ips
Binary file not shown.
BIN
public/link.ips
Normal file
BIN
public/link.ips
Normal file
Binary file not shown.
@@ -7,6 +7,8 @@
|
||||
@import './layout/news.css';
|
||||
@import './layout/activity.css';
|
||||
@import './layout/submit.css';
|
||||
@import './layout/reviews.css';
|
||||
@import './layout/responsive.css';
|
||||
|
||||
@import './components/common.css';
|
||||
@import './components/grid.css';
|
||||
|
||||
@@ -28,6 +28,9 @@
|
||||
/* Menu settings */
|
||||
--menu-size: 260px;
|
||||
--menu-user-avatar-bg: #555;
|
||||
|
||||
/* Gap */
|
||||
--gap: 15px;
|
||||
}
|
||||
|
||||
.light-mode {
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
background-color: var(--bg2);
|
||||
border: 1px solid var(--border);
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
transition: transform 0.2s, border-color 0.2s;
|
||||
cursor: pointer;
|
||||
@@ -41,6 +42,7 @@
|
||||
.entry-cover-wrapper {
|
||||
position: relative;
|
||||
aspect-ratio: 4/3;
|
||||
min-width: 0;
|
||||
background-color: var(--bg);
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
@@ -80,6 +82,7 @@
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 5px;
|
||||
line-height: 1.3;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.entry-card-author {
|
||||
@@ -97,3 +100,83 @@
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||
gap: 15px;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
padding: 15px;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
padding: 12px;
|
||||
gap: 10px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.stat-card i {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.entry-card-info {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.entry-card-title {
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.entry-card-author {
|
||||
font-size: 0.8rem;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.entry-card-meta {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 10px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
padding: 10px;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.entry-card {
|
||||
&:hover {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
.entry-card-title {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.entry-badge {
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
padding: 3px 6px;
|
||||
font-size: 0.65rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,6 +146,7 @@
|
||||
|
||||
.breadcrumb {
|
||||
margin-bottom: 15px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* PAGE */
|
||||
@@ -155,6 +156,7 @@
|
||||
font-weight: 300;
|
||||
margin-bottom: 20px;
|
||||
color: var(--text);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* TEXTS */
|
||||
@@ -193,3 +195,88 @@
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.btn {
|
||||
padding: 7px 12px;
|
||||
font-size: 0.85rem;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.block {
|
||||
padding: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.block-header {
|
||||
font-size: 1.05rem;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.content-title {
|
||||
margin: 20px 0 12px 0;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.quote {
|
||||
padding: 12px;
|
||||
margin-top: 20px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.whisper {
|
||||
margin-bottom: 12px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.btn {
|
||||
padding: 6px 10px;
|
||||
font-size: 0.8rem;
|
||||
gap: 4px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn.primary, .btn.danger, .btn.success {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.block {
|
||||
padding: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.block-header {
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.badge {
|
||||
padding: 2px 6px;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.topbar-badge {
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
padding: 0 3px;
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,6 +259,10 @@
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.database-wrapper {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.database-filters {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
@@ -275,19 +279,63 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.database-search {
|
||||
gap: 8px;
|
||||
margin-bottom: 15px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.database-wrapper {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.database-filters {
|
||||
width: 100%;
|
||||
grid-template-columns: 1fr;
|
||||
order: -1;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.database-filter-group {
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.grid-entries {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.database-search {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.database-filters {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.grid-entries {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.database-filter-group {
|
||||
padding: 10px 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 420px) {
|
||||
.grid-entries {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.database-search input {
|
||||
font-size: 0.85rem;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
margin-bottom: 6px;
|
||||
white-space: nowrap;
|
||||
white-space: normal;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
@@ -158,3 +158,103 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.drafts-count {
|
||||
font-size: 0.8rem;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.drafts-item {
|
||||
gap: 15px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.drafts-cover {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
}
|
||||
|
||||
.drafts-top {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.drafts-title {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.drafts-meta {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.drafts-actions {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.drafts-actions .btn {
|
||||
padding: 6px 10px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.drafts-empty {
|
||||
padding: 60px 15px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.drafts-empty h3 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.drafts-empty p {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.drafts-item {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.drafts-cover {
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.drafts-top {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.drafts-title {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.drafts-meta {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.drafts-progress {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.drafts-progress-bar {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.drafts-actions {
|
||||
flex-direction: row;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.drafts-actions .btn {
|
||||
flex: 1;
|
||||
min-width: 80px;
|
||||
padding: 5px 8px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -508,6 +508,17 @@
|
||||
flex-direction: row;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.upload-item-actions {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.upload-item-actions .btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.file-state-icon { width: 18px; height: 18px; }
|
||||
.file-state-icon--public { color: var(--success); }
|
||||
.file-state-icon--private { color: var(--text2); }
|
||||
@@ -613,3 +624,86 @@
|
||||
.game-selector-platform-only {
|
||||
grid-column: span 1;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.form-group.level {
|
||||
padding: 20px;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.form-group-title {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.form-group label, .form-label {
|
||||
margin-bottom: 6px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.form-input, .form-select, .form-textarea, .form-field {
|
||||
padding: 8px 10px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.game-selector-mode {
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.game-selector-mode-btn {
|
||||
padding: 10px 12px;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.game-selector-mode-btn:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.submit, .submit-level, .main-image-grid {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.grid-hashes {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.hash-first {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form-group.level {
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group-title {
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.form-group label, .form-label {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.form-input, .form-select, .form-textarea, .form-field {
|
||||
padding: 6px 8px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.form-error-text {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
.hovercard-overlay {
|
||||
position: absolute;
|
||||
z-index: 2000;
|
||||
position: fixed;
|
||||
z-index: 3500;
|
||||
background-color: var(--bg2);
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.hovercard-overlay-loading {
|
||||
@@ -117,3 +118,35 @@
|
||||
justify-content: center;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.hovercard {
|
||||
width: 260px;
|
||||
}
|
||||
|
||||
.hovercard-actions {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.hovercard-actions .btn {
|
||||
font-size: 0.75rem;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.hovercard {
|
||||
width: calc(100vw - 40px);
|
||||
max-width: 280px;
|
||||
}
|
||||
|
||||
.hovercard-actions {
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.hovercard-actions .btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -547,3 +547,128 @@
|
||||
padding: 14px 0 4px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.modcp-wrapper {
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.modcp-sidebar {
|
||||
width: 200px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.modcp-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.modcp-page-title {
|
||||
font-size: 1.15rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.modcp-wrapper {
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.modcp-sidebar {
|
||||
width: 100%;
|
||||
flex-shrink: 1;
|
||||
position: relative;
|
||||
top: auto;
|
||||
align-self: auto;
|
||||
margin-right: 0;
|
||||
margin-bottom: 15px;
|
||||
border: 1px solid var(--border);
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modcp-sidebar-header {
|
||||
padding: 12px 14px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.modcp-nav-label {
|
||||
padding: 6px 14px 3px;
|
||||
font-size: 0.65rem;
|
||||
}
|
||||
|
||||
.modcp-nav-item {
|
||||
padding: 6px 14px;
|
||||
font-size: 0.8rem;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.modcp-content {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.modcp-page-title {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.modcp-page-actions {
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.modcp-table {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.modcp-table th, .modcp-table td {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.modcp-table tbody tr {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.modcp-sidebar {
|
||||
max-height: 200px;
|
||||
}
|
||||
|
||||
.modcp-sidebar-header {
|
||||
padding: 10px 12px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.modcp-nav-item {
|
||||
padding: 5px 12px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.modcp-content {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.modcp-page-title {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.modcp-table {
|
||||
font-size: 0.8rem;
|
||||
overflow-x: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.modcp-table th, .modcp-table td {
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.log-diff-key {
|
||||
width: auto;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.log-raw {
|
||||
font-size: 0.7rem;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,28 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.notifications, .conversations {
|
||||
position: fixed;
|
||||
width: calc(100% - 30px);
|
||||
max-width: 340px;
|
||||
right: 15px;
|
||||
top: auto;
|
||||
bottom: 15px;
|
||||
max-height: calc(100vh - 130px);
|
||||
z-index: 3000 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.notifications, .conversations {
|
||||
width: calc(100% - 20px);
|
||||
right: 10px;
|
||||
bottom: 10px;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dropdown-enter {
|
||||
from { opacity: 0; transform: translateY(-6px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
|
||||
@@ -185,3 +185,82 @@
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.queue-item {
|
||||
padding: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.queue-item-header {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.queue-item-title {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.queue-item-meta {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.queue-item-actions-header {
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.timeline {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.timeline-container {
|
||||
padding: 12px 15px;
|
||||
}
|
||||
|
||||
.queue-mod-actions {
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.queue-empty {
|
||||
padding: 60px 15px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.queue-item {
|
||||
padding: 12px;
|
||||
border-left-width: 3px;
|
||||
}
|
||||
|
||||
.queue-item-title {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.queue-item-meta {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.queue-item-actions-header {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.timeline {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.timeline-container {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.queue-mod-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.queue-mod-actions .btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,29 @@
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.settings-dropdown {
|
||||
position: fixed;
|
||||
width: calc(100% - 30px);
|
||||
max-width: 240px;
|
||||
right: 15px;
|
||||
top: auto;
|
||||
bottom: 15px;
|
||||
max-height: calc(100vh - 130px);
|
||||
overflow-y: auto;
|
||||
z-index: 3000 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.settings-dropdown {
|
||||
width: calc(100% - 20px);
|
||||
right: 10px;
|
||||
bottom: 10px;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-header {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
|
||||
@@ -15,6 +15,62 @@
|
||||
.patcher-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.patcher-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.patcher-dropzone {
|
||||
padding: 40px 15px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.embed-patch-box {
|
||||
padding: 20px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.embed-patch-box-icon {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.embed-patch-box-icon-block {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.patcher-container {
|
||||
padding: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.patcher-grid {
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.patcher-dropzone {
|
||||
padding: 30px 12px;
|
||||
gap: 10px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.patcher-status-box {
|
||||
margin-top: 15px;
|
||||
padding: 12px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.embed-patch-box {
|
||||
padding: 15px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
padding: 6px 8px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
.patcher-dropzone {
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
color: var(--rhpz-orange);
|
||||
}
|
||||
|
||||
.activity-tl-dot--news {
|
||||
.activity-tl-dot--news, .activity-tl-dot--review {
|
||||
background-color: rgba(129,199,132,0.1);
|
||||
border-color: rgba(129,199,132,0.4);
|
||||
color: var(--success);
|
||||
@@ -198,7 +198,7 @@
|
||||
border: 1px solid rgba(255,115,0,0.25);
|
||||
}
|
||||
|
||||
.activity-tl-badge--news {
|
||||
.activity-tl-badge--news, .activity-tl-badge--review {
|
||||
background-color: rgba(129,199,132,0.1);
|
||||
color: var(--success);
|
||||
border: 1px solid rgba(129,199,132,0.25);
|
||||
@@ -267,6 +267,32 @@
|
||||
.activity-tl-thumb { display: none; }
|
||||
.activity-day-sep { padding-left: 44px; }
|
||||
.activity-tl-left { width: 44px; }
|
||||
|
||||
.activity-tl-date {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.activity-tl-content-title {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.activity-timeline {
|
||||
padding-left: 50px;
|
||||
}
|
||||
|
||||
.activity-tl-left {
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.activity-tl-header {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.activity-tl-date {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
.home-section {
|
||||
@@ -478,8 +504,52 @@
|
||||
.featured-entries-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.news-strip { grid-template-columns: repeat(2, 1fr); }
|
||||
.featured-entries-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
.news-strip-cover { height: 80px; }
|
||||
@media (max-width: 768px) {
|
||||
.news-strip {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.featured-entries-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.home-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.news-strip-cover {
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.featured-entry-title {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.news-strip { grid-template-columns: 1fr; }
|
||||
.featured-entries-grid { grid-template-columns: 1fr; }
|
||||
.news-strip-cover { height: 80px; }
|
||||
|
||||
.news-strip-item {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.news-strip-title {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.featured-entry-title {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.featured-entry-meta {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.home-section-title {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,155 @@
|
||||
}
|
||||
}
|
||||
|
||||
.topbar-more-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.topbar-more-menu {
|
||||
position: fixed;
|
||||
top: 60px;
|
||||
right: 0;
|
||||
background-color: var(--bg2);
|
||||
border: 1px solid var(--border);
|
||||
border-top: none;
|
||||
border-right: none;
|
||||
z-index: 2000;
|
||||
min-width: 180px;
|
||||
max-height: calc(100vh - 60px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.topbar-more-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
color: var(--text2);
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
transition: all 0.15s;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bg3);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
i {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
span {
|
||||
flex-grow: 1;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 769px) {
|
||||
.topbar-more-container {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.topbar-admin-btn,
|
||||
.topbar-mod-btn {
|
||||
display: flex !important;
|
||||
}
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.topbar-more-container {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.topbar-admin-btn,
|
||||
.topbar-mod-btn {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#topbar {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.topbar-actions {
|
||||
gap: 8px !important;
|
||||
}
|
||||
|
||||
.topbar-actions .btn {
|
||||
padding: 8px 6px;
|
||||
font-size: 0.85rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.topbar-actions i {
|
||||
width: 16px !important;
|
||||
height: 16px !important;
|
||||
}
|
||||
|
||||
.topbar-badge {
|
||||
font-size: 0.65rem;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
#topbar {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.topbar-actions {
|
||||
gap: 8px !important;
|
||||
}
|
||||
|
||||
.topbar-actions .btn {
|
||||
padding: 6px 4px;
|
||||
font-size: 0.75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.topbar-actions i {
|
||||
width: 14px !important;
|
||||
height: 14px !important;
|
||||
}
|
||||
|
||||
.topbar-badge {
|
||||
font-size: 0.6rem;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.search-scope-select {
|
||||
background-color: var(--bg2);
|
||||
border: none;
|
||||
border-right: 1px solid var(--border);
|
||||
color: var(--text2);
|
||||
font-size: 0.8rem;
|
||||
padding: 8px 10px;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
appearance: none;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
|
||||
.search-scope-select:hover,
|
||||
.search-scope-select:focus {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
#content {
|
||||
flex-grow: 1;
|
||||
padding: 30px;
|
||||
|
||||
@@ -268,36 +268,6 @@
|
||||
color: var(--text);
|
||||
line-height: 1.5;
|
||||
word-wrap: break-word;
|
||||
|
||||
p {
|
||||
margin-bottom: 10px;
|
||||
&:last-child { margin-bottom: 0; }
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--rhpz-orange);
|
||||
&:hover {
|
||||
color: var(--rhpz-orange-hover);
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
blockquote, .bbCodeBlock-blockquote {
|
||||
background-color: var(--bg);
|
||||
border-left: 3px solid var(--info);
|
||||
padding: 12px 16px;
|
||||
margin: 12px 0;
|
||||
font-style: italic;
|
||||
color: var(--text2);
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: monospace;
|
||||
background-color: var(--bg3);
|
||||
border: 1px solid var(--border);
|
||||
padding: 2px 5px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -381,3 +351,242 @@
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.markdown-body {
|
||||
p {
|
||||
margin-bottom: 10px;
|
||||
&:last-child { margin-bottom: 0; }
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--rhpz-orange);
|
||||
&:hover {
|
||||
color: var(--rhpz-orange-hover);
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
blockquote, .bbCodeBlock-blockquote {
|
||||
background-color: var(--bg);
|
||||
border-left: 3px solid var(--info);
|
||||
padding: 12px 16px;
|
||||
margin: 12px 0;
|
||||
font-style: italic;
|
||||
color: var(--text2);
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: monospace;
|
||||
background-color: var(--bg3);
|
||||
border: 1px solid var(--border);
|
||||
padding: 2px 5px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
.markdown-body h1, .markdown-body h2, .markdown-body h3,
|
||||
.markdown-body h4, .markdown-body h5, .markdown-body h6 {
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
margin: 16px 0 8px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.markdown-body h1 { font-size: 1.4rem; }
|
||||
.markdown-body h2 { font-size: 1.2rem; }
|
||||
.markdown-body h3 { font-size: 1.05rem; }
|
||||
|
||||
.markdown-body strong { color: var(--text); font-weight: 700; }
|
||||
.markdown-body em { color: var(--text2); }
|
||||
|
||||
.markdown-body ul, .markdown-body ol {
|
||||
margin: 0 0 12px 20px;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.markdown-body li { margin-bottom: 4px; line-height: 1.5; }
|
||||
|
||||
.markdown-body hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--border);
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.markdown-body table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 12px 0;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.markdown-body th, .markdown-body td {
|
||||
border: 1px solid var(--border);
|
||||
padding: 6px 10px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.markdown-body th {
|
||||
background-color: var(--bg3);
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.markdown-body del {
|
||||
color: var(--text2);
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.markdown-body img {
|
||||
max-width: 100%;
|
||||
border: 1px solid var(--border);
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.hack-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.entry-header {
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
gap: 20px;
|
||||
|
||||
.entry-cover {
|
||||
width: 100%;
|
||||
height: 280px;
|
||||
max-width: 300px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.entry-info {
|
||||
.entry-title {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
.entry-authors {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.entry-meta-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.entry-actions {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.entry-content {
|
||||
padding: 20px;
|
||||
|
||||
.entry-section-title {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.entry-gallery {
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.comment-block {
|
||||
gap: 12px;
|
||||
padding: 15px 0;
|
||||
|
||||
.comment-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.comment-content {
|
||||
.comment-body {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.video-thumbnail-wrapper {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.gallery-modal-close {
|
||||
top: 10px;
|
||||
right: 15px;
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.hack-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.entry-header {
|
||||
padding: 15px;
|
||||
gap: 15px;
|
||||
|
||||
.entry-cover {
|
||||
height: 240px;
|
||||
}
|
||||
|
||||
.entry-info {
|
||||
.entry-title {
|
||||
font-size: 1.3rem;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.entry-authors {
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.entry-actions {
|
||||
gap: 8px;
|
||||
|
||||
.btn {
|
||||
padding: 8px 12px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.entry-content {
|
||||
padding: 15px;
|
||||
|
||||
.entry-gallery {
|
||||
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
||||
gap: 8px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.comment-block {
|
||||
padding: 10px 0;
|
||||
|
||||
.comment-avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
.markdown-body h1 { font-size: 1.15rem; }
|
||||
.markdown-body h2 { font-size: 1rem; }
|
||||
.markdown-body h3 { font-size: 0.95rem; }
|
||||
|
||||
.hack-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -77,6 +77,21 @@
|
||||
color: var(--text);
|
||||
margin-bottom: 12px;
|
||||
text-shadow: 0 2px 4px rgba(0,0,0,0.6);
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.news-header .news-title {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.news-header .news-title {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.news-header .news-meta {
|
||||
@@ -298,7 +313,6 @@
|
||||
color: #e57373;
|
||||
}
|
||||
|
||||
/* ── Hero ────────────────────────────────────────────────── */
|
||||
.news-hero {
|
||||
display: block;
|
||||
position: relative;
|
||||
@@ -484,3 +498,106 @@
|
||||
.news-hero-title { font-size: 1.4rem; }
|
||||
.news-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
|
||||
.news-header .news-meta {
|
||||
gap: 12px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.news-layout {
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.news-sidebar {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.news-content {
|
||||
padding: 25px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.news-body-text {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.sidebar-block {
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.news-header .news-meta {
|
||||
gap: 8px;
|
||||
font-size: 0.8rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.news-header .meta-item {
|
||||
padding: 3px 8px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.news-layout {
|
||||
gap: 15px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.news-main-content {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.news-content {
|
||||
padding: 15px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.news-body-text {
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.news-body-text p {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.news-sidebar {
|
||||
width: 100%;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.sidebar-block {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.sidebar-block h3 {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.sidebar-block p {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.news-card-title {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 420px) {
|
||||
|
||||
.news-header .news-meta {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.news-layout {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.news-content {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
270
resources/css/layout/responsive.css
Normal file
270
resources/css/layout/responsive.css
Normal file
@@ -0,0 +1,270 @@
|
||||
|
||||
@media (max-width: 768px) {
|
||||
:root {
|
||||
--menu-size: 280px;
|
||||
}
|
||||
|
||||
#menu {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 60px;
|
||||
height: calc(100vh - 60px);
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.3s ease-in-out;
|
||||
z-index: 999;
|
||||
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
#menu.mobile-open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
#app.menu-open::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: 60px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 998;
|
||||
}
|
||||
|
||||
.mobile-toggle {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#main-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
#topbar {
|
||||
padding: 0 10px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
width: 100%;
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
.search-scope-select {
|
||||
font-size: 0.7rem;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.topbar-actions {
|
||||
gap: 4px;
|
||||
overflow-x: auto;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
.topbar-actions .btn {
|
||||
flex-shrink: 0;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.vertical-separator {
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
:root {
|
||||
--menu-size: 240px;
|
||||
}
|
||||
|
||||
#content {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
#topbar {
|
||||
padding: 0 8px;
|
||||
height: 55px;
|
||||
}
|
||||
|
||||
#topbar {
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
max-width: 100%;
|
||||
order: 3;
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.topbar-actions {
|
||||
gap: 2px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.topbar-actions .btn {
|
||||
padding: 4px 6px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.search-scope-select {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.search-bar input {
|
||||
padding: 4px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
#menu {
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
.menu-title {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.menu-logo {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.menu-header {
|
||||
padding: 8px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.menu-user-info .username {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 420px) {
|
||||
:root {
|
||||
--menu-size: 200px;
|
||||
}
|
||||
|
||||
#content {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
#topbar {
|
||||
padding: 0 6px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.mobile-toggle {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
.topbar-actions .btn {
|
||||
padding: 3px 4px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.vertical-separator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
padding: 8px 12px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.menu-group-title {
|
||||
padding: 0 12px;
|
||||
font-size: 0.65rem;
|
||||
}
|
||||
|
||||
.menu-user-info {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 500px) and (max-width: 768px) {
|
||||
#topbar {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
#content {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.menu-header {
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) and (min-width: 769px) {
|
||||
:root {
|
||||
--menu-size: 240px;
|
||||
}
|
||||
|
||||
#content {
|
||||
padding: 25px;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
width: 250px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 769px) {
|
||||
|
||||
#menu {
|
||||
transform: translateX(0) !important;
|
||||
position: relative !important;
|
||||
top: auto !important;
|
||||
height: auto !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
|
||||
#app.menu-open::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
.mobile-toggle {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
|
||||
#app {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#main-wrapper {
|
||||
width: calc(100% - var(--menu-size));
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
|
||||
.btn,
|
||||
.menu-item,
|
||||
button {
|
||||
min-height: 44px;
|
||||
min-width: 44px;
|
||||
}
|
||||
|
||||
|
||||
.btn {
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
|
||||
.menu-item:hover {
|
||||
background-color: var(--bg2);
|
||||
}
|
||||
}
|
||||
156
resources/css/layout/reviews.css
Normal file
156
resources/css/layout/reviews.css
Normal file
@@ -0,0 +1,156 @@
|
||||
.review-section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.review-header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.review-avg-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--rhpz-orange);
|
||||
background-color: rgba(255,115,0,.1);
|
||||
border: 1px solid rgba(255,115,0,.3);
|
||||
padding: 4px 10px;
|
||||
}
|
||||
|
||||
.review-avg-badge--lg {
|
||||
font-size: 1rem;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.review-avg-count {
|
||||
color: var(--text2);
|
||||
font-weight: 400;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.star-rating-display {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.star-rating-display .star-filled { color: var(--rhpz-orange); fill: var(--rhpz-orange); }
|
||||
.star-rating-display .star-empty { color: var(--border); }
|
||||
|
||||
.review-title {
|
||||
font-size: 0.98rem;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.star-input {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.star-input-icon.star-filled svg { color: var(--rhpz-orange); fill: var(--rhpz-orange); }
|
||||
.star-input-icon.star-empty svg { color: var(--border); }
|
||||
|
||||
.star-input-icon {
|
||||
cursor: pointer;
|
||||
transition: transform 0.1s;
|
||||
}
|
||||
|
||||
.star-input-icon:hover {
|
||||
transform: scale(1.15);
|
||||
}
|
||||
|
||||
.reviews-page-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-bottom: 25px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.reviews-back-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 0.82rem;
|
||||
color: var(--text2);
|
||||
text-decoration: none;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.reviews-back-link:hover { color: var(--rhpz-orange); }
|
||||
|
||||
.reviews-page-title {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.review-section-header {
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.review-header-right {
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.review-avg-badge {
|
||||
font-size: 0.8rem;
|
||||
padding: 3px 8px;
|
||||
}
|
||||
|
||||
.review-avg-badge--lg {
|
||||
font-size: 0.95rem;
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
.reviews-page-header {
|
||||
gap: 8px;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.reviews-page-title {
|
||||
font-size: 1.15rem;
|
||||
}
|
||||
|
||||
.review-title {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.review-avg-badge {
|
||||
font-size: 0.75rem;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
|
||||
.reviews-page-title {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.review-title {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.star-rating-display {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.star-input {
|
||||
gap: 3px;
|
||||
}
|
||||
}
|
||||
@@ -289,9 +289,60 @@
|
||||
.submit-rule:last-child { border-bottom: none; }
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.submit-hero {
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
padding: 25px 20px;
|
||||
}
|
||||
|
||||
.submit-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.submit-body {
|
||||
padding: 25px 20px;
|
||||
}
|
||||
|
||||
.submit-rules {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.submit-rule {
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.submit-hero, .submit-body { padding-left: 20px; padding-right: 20px; }
|
||||
.submit-hero, .submit-body {
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
.submit-grid { grid-template-columns: 1fr; }
|
||||
.submit-news-row { grid-template-columns: 1fr; }
|
||||
.submit-review-note { max-width: 100%; }
|
||||
|
||||
.submit-hero {
|
||||
gap: 15px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.submit-body {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.submit-rule {
|
||||
padding: 12px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.submit-hero-title {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.submit-grid > * {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
47
resources/js/HashesChecker.js
Normal file
47
resources/js/HashesChecker.js
Normal file
@@ -0,0 +1,47 @@
|
||||
import { calculate as calculateHashes } from "./hashes.js";
|
||||
|
||||
window.HashesChecker = function( 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(e){
|
||||
if( this.isCalculating === true ) // Calculation already done for another file.
|
||||
return;
|
||||
|
||||
this.error = null; // Reset.
|
||||
const FILE = e.target.files[0];
|
||||
|
||||
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.
|
||||
window.refreshIcons();
|
||||
} catch(err) {
|
||||
this.error = err.message;
|
||||
} finally {
|
||||
this.isCalculating = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import hovercard from "./hovercard.js";
|
||||
import notifications from "./notifications.js";
|
||||
import conversations from "./conversations.js";
|
||||
import settings from "./settings.js";
|
||||
import { initMobileMenu } from "./mobile-menu.js";
|
||||
|
||||
/**
|
||||
* Get config defined in meta.blade.php
|
||||
@@ -43,3 +44,6 @@ Alpine.store('conversations', conversations() );
|
||||
|
||||
// Settings
|
||||
Alpine.store('settings', settings() );
|
||||
|
||||
// Mobile Menu
|
||||
document.addEventListener('DOMContentLoaded', initMobileMenu);
|
||||
|
||||
46
resources/js/mobile-menu.js
Normal file
46
resources/js/mobile-menu.js
Normal file
@@ -0,0 +1,46 @@
|
||||
export function initMobileMenu() {
|
||||
const menuToggle = document.querySelector('.mobile-toggle');
|
||||
const menu = document.getElementById('menu');
|
||||
const app = document.getElementById('app');
|
||||
const content = document.getElementById('content');
|
||||
|
||||
if (!menuToggle || !menu) return;
|
||||
|
||||
menuToggle.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
menu.classList.toggle('mobile-open');
|
||||
app.classList.toggle('menu-open');
|
||||
});
|
||||
|
||||
const menuItems = menu.querySelectorAll('.menu-item');
|
||||
menuItems.forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
menu.classList.remove('mobile-open');
|
||||
app.classList.remove('menu-open');
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
const isClickInsideMenu = menu.contains(e.target);
|
||||
const isClickOnToggle = menuToggle.contains(e.target);
|
||||
|
||||
if (!isClickInsideMenu && !isClickOnToggle && menu.classList.contains('mobile-open')) {
|
||||
menu.classList.remove('mobile-open');
|
||||
app.classList.remove('menu-open');
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && menu.classList.contains('mobile-open')) {
|
||||
menu.classList.remove('mobile-open');
|
||||
app.classList.remove('menu-open');
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
if (window.innerWidth > 768) {
|
||||
menu.classList.remove('mobile-open');
|
||||
app.classList.remove('menu-open');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -27,7 +27,7 @@ const ERROR_TABLE = {
|
||||
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.",
|
||||
noMainImage: "Please upload a main image.",
|
||||
noGalleryImages: "Please select at least a gallery image.",
|
||||
isSubmitting: "The entry is already during submission."
|
||||
}
|
||||
@@ -269,75 +269,75 @@ window.Submission = function(){
|
||||
*/
|
||||
verifyForm(){
|
||||
|
||||
console.log( "Step 1" );
|
||||
console.info( "Step 1: During File upload" );
|
||||
if( !SubmissionVerifications.step1_DuringFSUpload( this.Uploader ) ){
|
||||
this.errorKey = "isUploading";
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log( "Step 2" );
|
||||
console.info( "Step 2: No files uploaded" );
|
||||
if( !SubmissionVerifications.step2_NoFilesFSUpload( this.Uploader ) ){
|
||||
this.errorKey = "noFiles";
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log( "Step 3" );
|
||||
console.info( 'Step 3: Error in file upload')
|
||||
if( !SubmissionVerifications.step3_ErrorsFSUpload( this.Uploader ) ){
|
||||
this.errorKey = "uploadError";
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log( "Step 4" );
|
||||
console.info("Step 4: All files uploaded");
|
||||
if( !SubmissionVerifications.step4_AllFilesUploadedFSUpload( this.Uploader ) ){
|
||||
this.errorKey = "notAllFilesDone";
|
||||
return false;
|
||||
}
|
||||
|
||||
if( SECTION() === "romhacks" || SECTION() === "lua-scripts" ){
|
||||
console.log( "Step 5" );
|
||||
console.info( "Step 5: Verify modifications")
|
||||
if( !SubmissionVerifications.step5_RomhacksModificationsCheckboxes()){
|
||||
this.errorKey = "noModifications";
|
||||
return false;
|
||||
}
|
||||
} else if( SECTION() === "utilities" ){
|
||||
console.log( "Step 5" );
|
||||
console.info( "Step 5: Verify systems");
|
||||
if( !SubmissionVerifications.step5_UtilitiesSystemsCheckboxes()){
|
||||
this.errorKey = "noSystems";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
console.log( "Step 6" );
|
||||
console.info( "Step 6: Verify description");
|
||||
if( !SubmissionVerifications.step6_VerifyDescription() ){
|
||||
this.errorKey = "noDescription";
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log( "Step 7" );
|
||||
console.info( "Step 7: Verify game");
|
||||
if( !SubmissionVerifications.step7_VerifyGame( this.$el ) ){
|
||||
this.errorKey = "noGame";
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log( "Step 8" );
|
||||
console.info("Step 8: Verify languages");
|
||||
if( !SubmissionVerifications.step8_LanguagesCheckboxes()){
|
||||
this.errorKey = "noLanguages";
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log( "Step 9" );
|
||||
console.info( "Step 9: Verify authors" );
|
||||
if( !SubmissionVerifications.step9_verifyAuthors()){
|
||||
this.errorKey = "noAuthors";
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log( "Step 10" );
|
||||
console.info( "Step 10: Verify Main image" );
|
||||
if( !SubmissionVerifications.step10_verifyMainImage( this.$el )){
|
||||
this.errorKey = "noMainImage";
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log( "Step 11" );
|
||||
console.info( "Step 11: Verify gallery images" );
|
||||
if( !SubmissionVerifications.step11_verifyGallery( this.$el )){
|
||||
this.errorKey = "noGalleryImages";
|
||||
return false;
|
||||
@@ -367,7 +367,11 @@ window.Submission = function(){
|
||||
isSubmitting: 'submitButton'
|
||||
};
|
||||
|
||||
const target = this.$refs[refMap[this.errorKey]]
|
||||
const targetKey = refMap[this.errorKey];
|
||||
|
||||
const target = this.$refs[targetKey]
|
||||
|| this.$el.querySelector(`[data-target="${targetKey}"]`)
|
||||
|| this.$el.querySelector(`[x-ref="${targetKey}"]`)
|
||||
|| this.$el.querySelector('.upload-list')
|
||||
|| this.$el.querySelector('.form-upload');
|
||||
|
||||
|
||||
@@ -31,6 +31,8 @@
|
||||
<i data-lucide="messages-square" size="14"></i>
|
||||
@elseif($item->type === 'club')
|
||||
<i data-lucide="balloon" size="14"></i>
|
||||
@elseif($item->type === 'review')
|
||||
<i data-lucide="star" size="14"></i>
|
||||
@else
|
||||
<i data-lucide="target" size="14"></i>
|
||||
@endif
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="filter-group" x-data="{open:false,search:''}">
|
||||
<div class="filter-group" x-data="{open:false}">
|
||||
<div class="filter-title-row" @click="open = !open">
|
||||
<div class="filter-title-left">
|
||||
<h4 class="filter-title">{{ $title }}</h4>
|
||||
@@ -18,11 +18,11 @@
|
||||
<div x-show="open" x-transition>
|
||||
<div class="internal-filter-search">
|
||||
<i data-lucide="search" size="13"></i>
|
||||
<input type="text" x-model="search" placeholder="Search...">
|
||||
<input type="text" wire:model.live.debounce.300ms="{{ $searchModel }}" placeholder="Search...">
|
||||
</div>
|
||||
<div class="filter-options">
|
||||
@foreach($items as $item)
|
||||
<label class="filter-option" x-show="search.length >= 3 && '{{strtolower($item->{$nameProperty}) }}'.includes(search.toLowerCase())">
|
||||
<label class="filter-option">
|
||||
<input type="checkbox" wire:model.live="{{ $model }}" value="{{ $item->{$idProperty} }}">
|
||||
{{ $item->{$nameProperty} }}
|
||||
</label>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="filter-group" x-data="{open: false,search:''}">
|
||||
<div class="filter-group" x-data="{open: false}">
|
||||
<div class="filter-title-row" @click="open = !open">
|
||||
<div class="filter-title-left">
|
||||
<h4 class="filter-title">{{ $title }}</h4>
|
||||
@@ -14,11 +14,11 @@
|
||||
<div x-show="open" x-transition>
|
||||
<div class="internal-filter-search">
|
||||
<i data-lucide="search" size="13"></i>
|
||||
<input type="text" x-model="search" placeholder="Search...">
|
||||
<input type="text" wire:model.live.debounce.300ms="{{ $searchModel }}" placeholder="Search...">
|
||||
</div>
|
||||
<div class="filter-options">
|
||||
@foreach($items as $item)
|
||||
<label class="filter-option" x-show="search.length >= 3 && '{{strtolower($item->{$nameProperty}) }}'.includes(search.toLowerCase())">
|
||||
<label class="filter-option">
|
||||
<input type="checkbox" wire:model.live="{{ $model }}" value="{{ $item->{$idProperty} }}">
|
||||
{{ $item->{$nameProperty} }}
|
||||
</label>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
@endif
|
||||
</div>
|
||||
<div class="entry-card-info">
|
||||
<a href="{{ route('entries.show', [ 'section' => $entry->type, 'entry' => $entry ] ) }}" class="entry-card-title">{{ $entry->title }}</a>
|
||||
<a href="{{ route('entries.show', [ 'section' => $entry->type, 'entry' => $entry ] ) }}" class="entry-card-title">{{ $entry->title ?? $entry->complete_title }}</a>
|
||||
<div class="entry-card-author">
|
||||
@forelse( $entry->authors as $author)
|
||||
@if($loop->first)By @endif
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<div class="block-error">
|
||||
<i data-lucide="{{ $errorArray['icon'] ?? '' }}"></i> {{ sprintf( $errorArray['message'], $message ) }}
|
||||
<i data-lucide="{{ $errorArray['icon'] ?? '' }}"></i> {!! sprintf( $errorArray['message'], $message ) !!}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div x-data="GalleryManager()" x-init="init(@js($oldPaths))">
|
||||
<div x-data="GalleryManager()" x-init="init(@js($oldPaths))" x-ref="gallery-field">
|
||||
<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 }">
|
||||
|
||||
@@ -18,12 +18,11 @@
|
||||
<i data-lucide="x"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="language-list" id="languages-group">
|
||||
<div class="language-list" x-ref="languagesGroup" 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>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<div x-data="MainImageManager()" x-init="init('{{$oldPath}}')">
|
||||
|
||||
<div x-data="MainImageManager()" x-init="init('{{$oldPath}}')" x-ref="main-image-field">
|
||||
<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;">
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user