feat: increase gallery grid from 4 to 5 columns per row on desktopfeat: increase gallery grid from 4 to 5 columns per row on desktop

This commit is contained in:
2026-02-25 19:11:23 +01:00
parent 5c97488e80
commit 0032aec02f
131 changed files with 15674 additions and 597 deletions

View File

@@ -0,0 +1,266 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
/**
* Migrates legacy `artworks_comments` (projekti_old_skinbase) into `artwork_comments`.
*
* Column mapping:
* legacy.comment_id artwork_comments.legacy_id (idempotency key)
* legacy.artwork_id artwork_comments.artwork_id
* legacy.user_id artwork_comments.user_id
* legacy.description artwork_comments.content
* legacy.date + .time artwork_comments.created_at / updated_at
*
* Ignored legacy columns: owner, author (username strings), owner_user_id
*
* Usage:
* php artisan comments:import-legacy
* php artisan comments:import-legacy --dry-run
* php artisan comments:import-legacy --chunk=1000
* php artisan comments:import-legacy --allow-guest-user=0 (import rows where user_id maps to 0 / not found, assigning a fallback user_id)
*/
class ImportLegacyComments extends Command
{
protected $signature = 'comments:import-legacy
{--dry-run : Preview only no writes to DB}
{--chunk=500 : Rows to process per batch}
{--skip-empty : Skip comments with empty/whitespace-only content}';
protected $description = 'Import legacy artworks_comments into artwork_comments';
public function handle(): int
{
$dryRun = (bool) $this->option('dry-run');
$chunk = max(1, (int) $this->option('chunk'));
$skipEmpty = (bool) $this->option('skip-empty');
if ($dryRun) {
$this->warn('[DRY-RUN] No data will be written.');
}
// Verify legacy connection
try {
DB::connection('legacy')->getPdo();
} catch (\Throwable $e) {
$this->error('Cannot connect to legacy database: ' . $e->getMessage());
return self::FAILURE;
}
if (! DB::connection('legacy')->getSchemaBuilder()->hasTable('artworks_comments')) {
$this->error('Legacy table `artworks_comments` not found.');
return self::FAILURE;
}
if (! DB::getSchemaBuilder()->hasColumn('artwork_comments', 'legacy_id')) {
$this->error('Column `legacy_id` missing from `artwork_comments`. Run: php artisan migrate');
return self::FAILURE;
}
// Pre-load valid artwork IDs and user IDs from new DB for O(1) lookup
$this->info('Loading new-DB artwork and user ID sets…');
$validArtworkIds = DB::table('artworks')
->whereNull('deleted_at')
->pluck('id')
->flip()
->all();
$validUserIds = DB::table('users')
->whereNull('deleted_at')
->pluck('id')
->flip()
->all();
$this->info(sprintf(
'Found %d artworks and %d users in new DB.',
count($validArtworkIds),
count($validUserIds)
));
// Already-imported legacy IDs (to resume safely)
$this->info('Loading already-imported legacy_ids…');
$alreadyImported = DB::table('artwork_comments')
->whereNotNull('legacy_id')
->pluck('legacy_id')
->flip()
->all();
$this->info(sprintf('%d comments already imported (will be skipped).', count($alreadyImported)));
$total = DB::connection('legacy')->table('artworks_comments')->count();
$this->info("Legacy rows to process: {$total}");
if ($total === 0) {
$this->warn('No legacy rows found. Nothing to do.');
return self::SUCCESS;
}
$stats = [
'imported' => 0,
'skipped_duplicate' => 0,
'skipped_artwork' => 0,
'skipped_user' => 0,
'skipped_empty' => 0,
'errors' => 0,
];
$bar = $this->output->createProgressBar($total);
$bar->setFormat(' %current%/%max% [%bar%] %percent:3s%% | imported: %imported% | skipped: %skipped%');
$bar->setMessage('0', 'imported');
$bar->setMessage('0', 'skipped');
$bar->start();
DB::connection('legacy')
->table('artworks_comments')
->orderBy('comment_id')
->chunk($chunk, function ($rows) use (
&$stats,
&$alreadyImported,
$validArtworkIds,
$validUserIds,
$dryRun,
$skipEmpty,
$bar
) {
$inserts = [];
$now = now();
foreach ($rows as $row) {
$legacyId = (int) $row->comment_id;
$artworkId = (int) $row->artwork_id;
$userId = (int) $row->user_id;
$content = trim((string) ($row->description ?? ''));
// --- Already imported ---
if (isset($alreadyImported[$legacyId])) {
$stats['skipped_duplicate']++;
$bar->advance();
continue;
}
// --- Content ---
if ($skipEmpty && $content === '') {
$stats['skipped_empty']++;
$bar->advance();
continue;
}
// Replace empty content with a placeholder so NOT NULL is satisfied
if ($content === '') {
$content = '[no content]';
}
// --- Artwork must exist ---
if (! isset($validArtworkIds[$artworkId])) {
$stats['skipped_artwork']++;
$bar->advance();
continue;
}
// --- User must exist ---
if (! isset($validUserIds[$userId])) {
$stats['skipped_user']++;
$bar->advance();
continue;
}
// --- Build timestamp from separate date + time columns ---
$createdAt = $this->buildTimestamp($row->date, $row->time, $now);
if (! $dryRun) {
$inserts[] = [
'legacy_id' => $legacyId,
'artwork_id' => $artworkId,
'user_id' => $userId,
'content' => $content,
'is_approved' => 1,
'created_at' => $createdAt,
'updated_at' => $createdAt,
'deleted_at' => null,
];
$alreadyImported[$legacyId] = true;
}
$stats['imported']++;
$bar->advance();
}
if (! $dryRun && ! empty($inserts)) {
try {
DB::table('artwork_comments')->insert($inserts);
} catch (\Throwable $e) {
// Fallback: row-by-row with ignore on unique violations
foreach ($inserts as $row) {
try {
DB::table('artwork_comments')->insertOrIgnore([$row]);
} catch (\Throwable) {
$stats['errors']++;
}
}
}
}
$skippedTotal = $stats['skipped_duplicate']
+ $stats['skipped_artwork']
+ $stats['skipped_user']
+ $stats['skipped_empty'];
$bar->setMessage((string) $stats['imported'], 'imported');
$bar->setMessage((string) $skippedTotal, 'skipped');
});
$bar->finish();
$this->newLine(2);
// -------------------------------------------------------------------------
// Summary
// -------------------------------------------------------------------------
$this->table(
['Result', 'Count'],
[
['Imported', $stats['imported']],
['Skipped already imported', $stats['skipped_duplicate']],
['Skipped artwork gone', $stats['skipped_artwork']],
['Skipped user gone', $stats['skipped_user']],
['Skipped empty content', $stats['skipped_empty']],
['Errors', $stats['errors']],
]
);
if ($dryRun) {
$this->warn('[DRY-RUN] Nothing was written. Re-run without --dry-run to apply.');
} else {
$this->info('Migration complete.');
}
return $stats['errors'] > 0 ? self::FAILURE : self::SUCCESS;
}
/**
* Combine a legacy `date` (DATE) and `time` (TIME) column into a single datetime string.
* Falls back to $fallback when both are null.
*/
private function buildTimestamp(mixed $date, mixed $time, \Illuminate\Support\Carbon $fallback): string
{
if (! $date) {
return $fallback->toDateTimeString();
}
$datePart = substr((string) $date, 0, 10); // '2000-09-13'
$timePart = $time ? substr((string) $time, 0, 8) : '00:00:00'; // '09:34:27'
// Sanity-check: MySQL TIME can be negative or > 24h for intervals — clamp to midnight
if (! preg_match('/^\d{2}:\d{2}:\d{2}$/', $timePart) || $timePart < '00:00:00') {
$timePart = '00:00:00';
}
return $datePart . ' ' . $timePart;
}
}