diff --git a/packages/category/composer.json b/packages/category/composer.json index 19b979037b..850e56e5d8 100644 --- a/packages/category/composer.json +++ b/packages/category/composer.json @@ -27,7 +27,8 @@ "autoload": { "psr-4": { "Moox\\Category\\": "src", - "Moox\\Category\\Database\\Factories\\": "database/factories" + "Moox\\Category\\Database\\Factories\\": "database/factories", + "Moox\\Category\\Database\\Seeders\\": "database/seeders" } }, "extra": { diff --git a/packages/category/database/seeders/CategorySeeder.php b/packages/category/database/seeders/CategorySeeder.php new file mode 100644 index 0000000000..903c8416e0 --- /dev/null +++ b/packages/category/database/seeders/CategorySeeder.php @@ -0,0 +1,561 @@ +seed(); + + if (class_exists(RunsMooxDemoAssets::class)) { + RunsMooxDemoAssets::invoke($this); + } + } + + protected function seed(): void + { + $total = $this->resolvedCount(); + + $this->purgeSeededCategories(); + + $user = $this->requireDemoAuthor(); + if ($user === null) { + return; + } + + $missingLocales = collect($this->locales()) + ->filter(fn (string $locale): bool => ! Localization::query()->where('locale_variant', $locale)->exists()); + + if ($missingLocales->isNotEmpty()) { + $this->command->error( + 'Missing `localizations` rows for: '.$missingLocales->implode(', '). + '. Add those locale_variant values before running this seeder.' + ); + + return; + } + + $mediaPool = $this->loadImageMediaPool(); + if ($mediaPool->isEmpty()) { + $this->command->warn('No images in `media` table — categories will be seeded without images / media_usables.'); + } + + Auth::login($user); + + $baseUrl = rtrim((string) config('app.url'), '/'); + $parentMap = self::buildParentIndexMap($total); + /** @var array $idByIndex */ + $idByIndex = []; + + $progress = $this->hasSeedOutput() + ? SeedOutput::progressBar($total, 'Demo categories') + : null; + + DB::transaction(function () use ($baseUrl, $total, $parentMap, $mediaPool, $user, $progress, &$idByIndex): void { + for ($i = 1; $i <= $total; $i++) { + $parentIndex = $parentMap[$i] ?? null; + $parentId = $parentIndex !== null ? ($idByIndex[$parentIndex] ?? null) : null; + + $translationStatuses = $this->translationStatusesForCategory(); + + $category = new Category; + $category->is_active = $this->randomChance(92); + $category->status = self::resolveCategoryStatusFromTranslationStatuses($translationStatuses); + $category->weight = $i; + $category->color = $this->randomHexColor(); + $category->due_at = $this->randomChance(35) + ? $this->randomDateTimeBetween('-3 months', '+6 months') + : null; + $category->custom_properties = [ + 'seed_batch' => self::SEED_BATCH, + 'featured' => $this->randomChance(18), + 'sort_hint' => random_int(1, 100), + ]; + $category->basedata = [ + 'seed_batch' => self::SEED_BATCH, + 'seed_index' => $i, + ]; + + if ($parentId !== null) { + $category->parent_id = $parentId; + } + + $shouldAttachMedia = $mediaPool->isNotEmpty() + && $this->randomChance((int) (self::MEDIA_ATTACH_PROBABILITY * 100)); + + foreach ($this->locales() as $locale) { + $localeFaker = $this->fakerForLocale($locale); + $title = $this->fakerLocaleTitle($locale, $localeFaker, 'title'); + $slug = $this->slugForTitle($title, $i, $locale); + + $translation = $category->translateOrNew($locale); + $translation->title = $title; + $translation->slug = $slug; + $translation->permalink = $baseUrl.'/'.Str::lower(str_replace('_', '-', $locale)).'/categories/'.$slug; + $translation->description = $this->fakerLocaleText($locale, $localeFaker, preset: 'description'); + $translation->content = $this->markdownContentFromLocale($locale, $localeFaker); + $this->applyTranslationStatus($translation, $translationStatuses[$locale]); + $this->assignTranslationAuthor($translation, $user); + } + + $category->save(); + + if ($shouldAttachMedia) { + AttachExistingMedia::attach( + $category, + $mediaPool->random(), + 'image', + $this->primaryMediaLocale(), + ); + } + + $idByIndex[$i] = (int) $category->getKey(); + + if ($progress !== null) { + $progress->advance(); + } elseif ($total > 50 && ($i % self::PROGRESS_LOG_EVERY === 0 || $i === $total)) { + $this->reportCreated("Category {$category->getKey()}"); + } + } + + Category::fixTree(); + + $seededIds = Category::query() + ->where('basedata->seed_batch', self::SEED_BATCH) + ->pluck('id'); + + if ($seededIds->isNotEmpty()) { + $childrenCountByParent = Category::query() + ->whereIn('parent_id', $seededIds) + ->selectRaw('parent_id, COUNT(*) as aggregate') + ->groupBy('parent_id') + ->pluck('aggregate', 'parent_id'); + + foreach ($seededIds as $seededId) { + Category::query() + ->whereKey((int) $seededId) + ->update([ + 'count' => (int) ($childrenCountByParent[$seededId] ?? 0), + ]); + } + } + }); + + Auth::logout(); + + $progress?->finish("{$total} demo categories"); + + $withMedia = DB::table('media_usables') + ->where('media_usable_type', Category::class) + ->whereIn('media_usable_id', Category::query() + ->where('basedata->seed_batch', self::SEED_BATCH) + ->pluck('id')) + ->count(); + + $this->reportDetail(sprintf( + 'Seeded %d categories (%d locales each), %d media_usables links, tree depth up to %d.', + $total, + count($this->locales()), + $withMedia, + self::MAX_TREE_DEPTH + )); + } + + /** + * @return array 1-based child index => 1-based parent index or null for root + */ + public static function buildParentIndexMap(int $total): array + { + if ($total < 1) { + return []; + } + + $rootCount = max(5, (int) round(sqrt($total) * 1.2)); + $rootCount = min($rootCount, $total); + + $map = []; + for ($i = 1; $i <= $rootCount; $i++) { + $map[$i] = null; + } + + for ($i = $rootCount + 1; $i <= $total; $i++) { + $candidates = self::parentCandidatesForChild($map, $i); + $targetRoot = (($i - 1) % $rootCount) + 1; + + $sameBranch = array_values(array_filter( + $candidates, + fn (int $candidate): bool => self::rootAncestorIndex($map, $candidate) === $targetRoot + )); + + if ($sameBranch !== []) { + $candidates = $sameBranch; + } + + $map[$i] = $candidates[array_rand($candidates)]; + } + + return $map; + } + + /** + * @param array $map + * @return list + */ + private static function parentCandidatesForChild(array $map, int $childIndex): array + { + $depthByIndex = self::depthsFromParentMap($map, $childIndex - 1); + $candidates = []; + + for ($candidate = 1; $candidate < $childIndex; $candidate++) { + $depth = $depthByIndex[$candidate] ?? 1; + if ($depth < self::MAX_TREE_DEPTH) { + $candidates[] = $candidate; + } + } + + if ($candidates === []) { + $candidates[] = max(1, $childIndex - 1); + } + + return $candidates; + } + + /** + * @param array $map + * @return array + */ + private static function depthsFromParentMap(array $map, int $maxIndex): array + { + $depths = []; + + for ($i = 1; $i <= $maxIndex; $i++) { + $parent = $map[$i] ?? null; + $depths[$i] = $parent === null ? 1 : (($depths[$parent] ?? 1) + 1); + } + + return $depths; + } + + private static function rootAncestorIndex(array $map, int $index): int + { + $current = $index; + + while (($map[$current] ?? null) !== null) { + $current = $map[$current]; + } + + return $current; + } + + private function slugForTitle(string $title, int $index, string $locale): string + { + $base = Str::slug($title); + + if ($base === '') { + $base = 'item-'.sprintf('%03d', $index); + } + + return Str::limit($base, 72, '').'-'.sprintf('%03d', $index); + } + + /** + * Remove prior demo categories so re-runs replace Latin/legacy rows (repeatable moox:demo). + */ + private function purgeSeededCategories(): void + { + $ids = Category::query() + ->where('basedata->seed_batch', self::SEED_BATCH) + ->pluck('id'); + + if ($ids->isEmpty()) { + return; + } + + CategoryTranslation::query() + ->whereIn('category_id', $ids) + ->forceDelete(); + + Category::query() + ->whereIn('id', $ids) + ->orderByDesc('_lft') + ->each(static fn (Category $category): bool => (bool) $category->forceDelete()); + + $this->reportDetail(sprintf('Purged %d prior demo categor(ies) (seed_batch %s).', $ids->count(), self::SEED_BATCH)); + } + + private function fakerForLocale(string $locale): Generator + { + static $cache = []; + $resolvedLocale = in_array($locale, $this->locales(), true) ? $locale : 'en_US'; + + if (! isset($cache[$resolvedLocale])) { + $cache[$resolvedLocale] = FakerFactory::create($resolvedLocale); + } + + return $cache[$resolvedLocale]; + } + + /** + * Mirrors {@see BaseDraftTranslationModel::checkAndUpdateMainEntryStatus()} + * for multi-locale categories after translations are saved. + * + * @param array $translationStatuses + */ + public static function resolveCategoryStatusFromTranslationStatuses(array $translationStatuses): string + { + $translationStatuses = array_values(array_filter( + $translationStatuses, + static fn (string $status): bool => $status !== '' + )); + + $count = count($translationStatuses); + + if ($count === 0) { + return 'draft'; + } + + if ($count === 1) { + return $translationStatuses[0]; + } + + $publishedCount = count(array_filter( + $translationStatuses, + static fn (string $status): bool => $status === 'published' + )); + + if ($publishedCount === $count) { + return 'published'; + } + + if ($publishedCount === 0) { + return self::mostCommonStatus($translationStatuses) ?? 'draft'; + } + + $unpublished = array_values(array_filter( + $translationStatuses, + static fn (string $status): bool => $status !== 'published' + )); + + return self::mostCommonStatus($unpublished) ?? 'draft'; + } + + /** + * @return array locale_variant => translation_status + */ + private function translationStatusesForCategory(): array + { + $roll = random_int(1, 100); + + if ($roll <= 28) { + return array_fill_keys($this->locales(), 'published'); + } + + if ($roll <= 48) { + return array_fill_keys($this->locales(), 'draft'); + } + + if ($roll <= 58) { + return array_fill_keys($this->locales(), 'waiting'); + } + + if ($roll <= 78) { + return $this->mixedTranslationStatuses(); + } + + if ($roll <= 92) { + return $this->mostlyPublishedTranslationStatuses(); + } + + return $this->oneScheduledTranslationStatuses(); + } + + /** + * @return array + */ + private function mixedTranslationStatuses(): array + { + $statuses = []; + + foreach ($this->locales() as $locale) { + $statuses[$locale] = $this->weightedTranslationStatus(); + } + + if (count(array_unique($statuses)) < 2) { + $statuses[$this->locales()[1]] = $statuses[$this->locales()[0]] === 'published' ? 'draft' : 'published'; + } + + return $statuses; + } + + /** + * @return array + */ + private function mostlyPublishedTranslationStatuses(): array + { + $statuses = array_fill_keys($this->locales(), 'published'); + $outlierLocale = $this->locales()[array_rand($this->locales())]; + $statuses[$outlierLocale] = $this->randomElement(['draft', 'waiting', 'scheduled', 'privat']); + + return $statuses; + } + + /** + * @return array + */ + private function oneScheduledTranslationStatuses(): array + { + $statuses = array_fill_keys($this->locales(), 'published'); + $statuses[$this->locales()[array_rand($this->locales())]] = 'scheduled'; + + if (count(array_filter($statuses, static fn (string $s): bool => $s === 'published')) === count($this->locales())) { + $statuses[$this->locales()[0]] = 'draft'; + } + + return $statuses; + } + + private function weightedTranslationStatus(): string + { + $roll = random_int(1, 100); + + if ($roll <= 38) { + return 'published'; + } + + if ($roll <= 73) { + return 'draft'; + } + + if ($roll <= 88) { + return 'waiting'; + } + + if ($roll <= 96) { + return 'scheduled'; + } + + return 'privat'; + } + + private function applyTranslationStatus( + BaseDraftTranslationModel $translation, + string $status, + ): void { + $translation->translation_status = $status; + + if ($status === 'scheduled') { + $translation->to_publish_at = $this->randomDateTimeBetween('+2 days', '+60 days'); + } + } + + /** + * @param list $statuses + */ + private static function mostCommonStatus(array $statuses): ?string + { + if ($statuses === []) { + return null; + } + + $counts = array_count_values($statuses); + arsort($counts); + + return array_key_first($counts); + } + + private function resolvedCount(): int + { + if ($this->count !== null) { + return max(1, min(5000, $this->count)); + } + + if (class_exists(SeedingConfig::class)) { + return SeedingConfig::resolveCount('category', 100); + } + + $fromConfig = config('category.seeder_count'); + if (is_numeric($fromConfig)) { + return max(1, min(5000, (int) $fromConfig)); + } + + return 100; + } + + private function randomChance(int $percent): bool + { + return random_int(1, 100) <= $percent; + } + + /** + * @template T + * + * @param list $items + * @return T + */ + private function randomElement(array $items): mixed + { + return $items[array_rand($items)]; + } + + private function randomHexColor(): string + { + return sprintf('#%06x', random_int(0, 0xFFFFFF)); + } + + private function randomDateTimeBetween(string $from, string $to): Carbon + { + $min = strtotime($from); + $max = strtotime($to); + + return Carbon::createFromTimestamp(random_int($min, $max)); + } +} diff --git a/packages/category/database/seeders/Support/AttachExistingMedia.php b/packages/category/database/seeders/Support/AttachExistingMedia.php new file mode 100644 index 0000000000..4c3053a1fc --- /dev/null +++ b/packages/category/database/seeders/Support/AttachExistingMedia.php @@ -0,0 +1,37 @@ +exists || $model->getKey() === null) { + return; + } + + DB::table('media_usables')->insertOrIgnore([ + 'media_id' => $media->getKey(), + 'media_usable_id' => $model->getKey(), + 'media_usable_type' => $model::class, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + if (! is_array($model->{$field} ?? null)) { + $model->forceFill([ + $field => [ + 'media_id' => $media->getKey(), + 'locale' => $locale, + ], + ]); + $model->saveQuietly(); + } + } +} diff --git a/packages/category/src/Models/Category.php b/packages/category/src/Models/Category.php index 51b8d62c75..8e0c9deb26 100644 --- a/packages/category/src/Models/Category.php +++ b/packages/category/src/Models/Category.php @@ -44,6 +44,7 @@ * @property-read Category|null $parent * * @method static CategoryFactory factory($count = null, $state = []) + * @method static void fixTree() */ class Category extends BaseDraftModel implements HasMedia { @@ -90,6 +91,7 @@ protected function getCustomTranslatedAttributes(): array 'is_active' => 'boolean', 'weight' => 'integer', 'count' => 'integer', + 'image' => 'json', 'basedata' => 'json', 'due_at' => 'datetime', 'uuid' => 'string', diff --git a/packages/demo/.gitignore b/packages/demo/.gitignore new file mode 100644 index 0000000000..f397794a6a --- /dev/null +++ b/packages/demo/.gitignore @@ -0,0 +1,50 @@ +# Environment +.env +.env.backup + +# Composer +/vendor +composer.lock +auth.json + +# NPM / Node +/node_modules +npm-debug.log +package-lock.json + +# Laravel +/public/hot +/public/storage +/storage/*.key + +# PHPUnit +.phpunit.result.cache +phpunit.xml + +# Yarn +yarn-error.log + +# PHPStan +/build +phpstan.neon + +# Testbench +testbench.yaml +/workbench/* + +# PHP CS Fixer +.php-cs-fixer.cache + +# Homestead +Homestead.json +Homestead.yaml + +# IDEs +/.idea +/.vscode + +# MacOS +.DS_Store + +# Windows +Thumbs.db diff --git a/packages/demo/README.md b/packages/demo/README.md index 9b6f2384db..480fc6cd56 100644 --- a/packages/demo/README.md +++ b/packages/demo/README.md @@ -1,69 +1,195 @@ # Moox Demo -Demo jobs and Artisan commands for testing [Moox Jobs](../jobs/README.md) (progress, batches, failures, timeouts). +Seed demo data for **installed** Moox packages: static reference data, localizations, package seeders (dependency-aware order), factory-generated entities, and optional demo media files. -Copy the jobs and commands into your Laravel app (`app/Jobs/`, `app/Console/Commands/`), then register the schedule in `app/Console/Kernel.php` (or your scheduler of choice). +Requires **`moox/core`**. Package discovery for `moox:demo` lives in this package (`Moox\Demo\Support\MooxPackageDiscovery`). Other Moox packages are used at runtime when installed (`moox/data`, `moox/localization`, `moox/media`, `moox/draft`, etc.). -## What each job simulates +For queue job examples, see **[Moox Jobs](../jobs/README.md)**. -### DemoJob (`moox:demojob`) +## Requirements -A long-running job with a progress bar (`JobProgress`): 10 steps of 10% each, with a 10-second pause between steps — useful for seeing “Running” jobs and progress in Filament. +- PHP 8.2+ +- Laravel 12+ (Moox dev app) +- `moox/core` +- Migrations for packages you want to seed (`php artisan moox:install` recommended) -### BatchJob (`moox:batchjob`) +### Optional packages (features) -Dispatches a Laravel job batch with 14× `ShortJob` — exercises the Batches view in Moox Jobs. +| Package | Used for | +|---------|----------| +| `moox/data` | Countries, languages, currencies (`DataSeeder`) | +| `moox/localization` | Localization records | +| `moox/media` | Mediathek + demo file import | +| `moox/category`, `moox/draft`, `moox/product`, … | Package seeders + factory entities | -### ShortJob +## Installation -A fast job with progress, batch-aware (exits early if the batch was cancelled). +```bash +composer require moox/demo +``` -### LongJob (`moox:longjob`) +In the Moox monorepo dev app, enable `demo` in `config/devlink.php` and run `composer update`. -A very long run (20 seconds per percentage step, 1200 s timeout) — simulates a slow or “stuck” long-running job. +No Filament entity is registered by this package. You do **not** need a separate `moox:install` step for `moox/demo` itself. -### FailJob (`moox:failjob`) +## Prerequisites -Throws an exception on purpose — ends up in Failed Jobs; useful for testing retries and backoff. +1. Install Moox packages you need (`php artisan moox:install`). +2. For category seeding, a user must exist — `moox:demo` can create a demo user (see [Configuration](#configuration)). -### TimeoutJob (`moox:timeoutjob`) +## Command: `php artisan moox:demo` -10 s timeout but runs longer (progress + `sleep`) — useful for testing timeout behavior. +Seeds the application using installed Moox packages. Packages that are not installed are skipped. -Each job sets `$tries`, `$timeout`, and `$backoff` so you can also observe retry logic in the UI. +### Options -## Commands +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `--languages` | integer | `3` | Number of localizations when `--locales` is omitted | +| `--locales` | string | — | Comma-separated locale variants (e.g. `de_DE,en_GB,fr_FR`). Overrides `--languages` | +| `--dataset` | string | `small` | Records per entity for factory seeding: `small`, `medium`, `large`, `huge` | +| `--fresh` | flag | `false` | Runs `migrate:fresh` before seeding (**destroys all data**) | +| `--skip-seeders` | flag | `false` | Skips package entry seeders | +| `--skip-factories` | flag | `false` | Skips factory-based entity seeding | +| `--skip-media` | flag | `false` | Skips copying files from `resources/demo/media/` | +| `-v`, `-vv` | flag | — | Verbose output (seeder order, skipped steps) | -| Command | Description | -| --- | --- | -| `moox:demojob` | Dispatch the demo job (progress) | -| `moox:batchjob` | Dispatch the batch job | -| `moox:failjob` | Dispatch a job that fails | -| `moox:longjob` | Dispatch a long-running job | -| `moox:shortjob` | Dispatch a short job | -| `moox:timeoutjob` | Dispatch a job that times out | +### Dataset sizes + +| `--dataset` | Records per factory entity | +|-------------|----------------------------| +| `small` | 100 | +| `medium` | 1,000 | +| `large` | 10,000 | +| `huge` | 100,000 | + +Configured in `config/demo.php` under `dataset_sizes`. + +### What the command does (pipeline) + +1. **`--fresh`** — optional `migrate:fresh --force` (with confirmation in interactive mode). +2. **`moox/data`** — runs `DataSeeder` only (static countries, languages, currencies, …). +3. **Localizations** — creates/updates rows for `--locales` or default locales (`de_DE`, `en_US`, `es_ES`). +4. **Demo media** — copies `resources/demo/media/*` to the configured storage disk. +5. **Demo user** — creates `demo@moox.org` if no user exists (when enabled in config). +6. **Other package seeders** — runs one **entry seeder** per installed Moox package in **dependency order** (topological sort + `config/demo.php` priorities). Skips nested seeders already called by `DataSeeder`. +7. **Factory seeding** — for packages with `extra.moox.install.auto_entities` and a model factory, creates `--dataset` records (with locales when factories support `withLocales()` / `withTranslationLocales()`). + +### Examples + +Minimal demo (3 locales, 100 records per factory entity): + +```bash +php artisan moox:demo +``` + +Custom locales and medium dataset: -Run manually, for example: +```bash +php artisan moox:demo --locales=de_DE,en_GB,fr_FR,ar_IR,es_ES --dataset=medium +``` + +Reset database and seed: ```bash -php artisan moox:demojob +php artisan moox:demo --fresh --dataset=small ``` -## Schedule +Only seeders and localizations (no factories, no media files): -Add this to the `schedule()` method in `app/Console/Kernel.php`: +```bash +php artisan moox:demo --skip-factories --skip-media +``` -```php -protected function schedule(Schedule $schedule): void -{ - $schedule->command('moox:batchjob')->daily(); - $schedule->command('moox:demojob')->hourly(); +Large stress test (can take a long time and use significant memory): - // Optional demo schedules (uncomment as needed): - // $schedule->command('moox:failjob')->cron('0 */3 * * *'); // Every 3 minutes - // $schedule->command('moox:longjob')->cron('0 */45 * * *'); // Every 45 minutes - // $schedule->command('moox:timeoutjob')->cron('0 */20 * * *'); // Every 20 minutes -} +```bash +php artisan moox:demo --dataset=huge ``` -Ensure the Laravel scheduler runs (`php artisan schedule:work` locally, or a cron entry for `schedule:run` in production). +## Seeder documentation + +**German, in-depth guide** (registration, pipeline, `UserSeeder` + `CategorySeeder` as reference implementations, Demo API, troubleshooting): + +→ **[docs/SEEDERS.md](docs/SEEDERS.md)** + +## Seeder order and dependencies + +Seeders are **not** run in alphabetical file order. `moox:demo` uses: + +- **Topological sort** of `moox/*` composer dependencies +- **One entry seeder per package** (`extra.moox.install.seed`, e.g. `DataSeeder`, not `StaticLanguageSeeder` alone) +- **Manual priority** via `seeder_order` in `config/demo.php` + +Typical order: + +```text +moox/data (DataSeeder) + → localizations (CLI step or LocalizationSeeder) + → demo media / moox/media + → demo user + → moox/attribute, moox/tag, moox/category, moox/draft, … + → factory loops (product, draft, …) +``` + +`CategorySeeder` expects users, localizations, and media — run `moox:demo` after `moox/data` and prefer having `moox/media` installed. + +## Demo media + +### Static asset packs (offline) + +Bundled under `resources/demo/assets/`: + +```text +assets/images/products/ # product / category photos +assets/images/users/ # user avatar photos +assets/files/pdf/ # PDF samples +assets/files/documents/ # txt, docx, xlsx +assets/files/audio/ # mp3 sample +assets/videos/short/ # mp4 / webm clips +``` + +Sources and licenses: [`resources/demo/assets/MEDIA_SOURCES.md`](resources/demo/assets/MEDIA_SOURCES.md). + +### Storage copy (root media folder) + +Files placed directly in `resources/demo/media/` (not subfolders) are copied to `storage` on the disk defined in `config/demo.php` (`media.disk`, `media.directory`). When `moox/media` is installed, attach media to entities via category/draft seeders or the Mediathek UI. + +## Configuration + +Publish config: + +```bash +php artisan vendor:publish --tag=demo-config +``` + +Key settings in `config/demo.php`: + +- `dataset_sizes` — map dataset name → record count +- `default_locales` / `default_language_count` +- `seeder_order` — slug priority list +- `seeder_skip` — packages never seeded by demo (e.g. `demo`, `core`) +- `nested_seeder_basenames` — seeders only invoked by a parent seeder +- `demo_user` — auto-create demo user for category seeding + +Factories can read `config('demo.locales')` and `config('demo.dataset_count')` during the factory step. + +## Troubleshooting + +| Issue | Action | +|-------|--------| +| No languages in `static_languages` | Install `moox/data`, run `moox:demo` (or `DataSeeder` first) | +| Category seeder fails / no user | Enable `demo_user` in config or run `php artisan make:filament-user` | +| `huge` runs out of memory or time | Use `medium` or `small`, or `--skip-factories` | +| Seeder class not found | Ensure `extra.moox.install.seed` points to a class under `Moox\{Package}\Database\Seeders` | +| Nothing seeded for a package | Package may not be installed or listed in `seeder_skip` | + +## Related commands + +| Command | Description | +|---------|-------------| +| `php artisan moox:install` | Install Moox packages (migrations, configs, plugins) | + +## License + +MIT. See [LICENSE.md](LICENSE.md) when present. diff --git a/packages/demo/composer.json b/packages/demo/composer.json new file mode 100644 index 0000000000..b8698c24ad --- /dev/null +++ b/packages/demo/composer.json @@ -0,0 +1,55 @@ +{ + "name": "moox/demo", + "description": "Seed demo data for installed Moox packages (localizations, seeders, factories, media).", + "keywords": [ + "Moox", + "Laravel", + "demo", + "seeder", + "Moox package", + "Laravel package" + ], + "homepage": "https://moox.org/docs/demo", + "license": "MIT", + "authors": [ + { + "name": "Moox Developer", + "email": "dev@moox.org", + "role": "Developer" + } + ], + "require": { + "moox/core": "dev-main" + }, + "autoload": { + "psr-4": { + "Moox\\Demo\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "Moox\\Demo\\Tests\\": "tests" + } + }, + "extra": { + "laravel": { + "providers": [ + "Moox\\Demo\\DemoServiceProvider" + ] + }, + "moox": { + "name": "Moox Demo", + "stability": "dev", + "type": "moox-package" + } + }, + "prefer-stable": true, + "require-dev": { + "moox/devtools": "dev-main" + }, + "config": { + "allow-plugins": { + "pestphp/pest-plugin": true + } + } +} diff --git a/packages/demo/config/demo.php b/packages/demo/config/demo.php new file mode 100644 index 0000000000..be3f79c7a6 --- /dev/null +++ b/packages/demo/config/demo.php @@ -0,0 +1,97 @@ + [ + 'small' => 100, + 'medium' => 1_000, + 'large' => 10_000, + 'huge' => 100_000, + ], + + 'default_dataset' => 'small', + + 'default_language_count' => 3, + + /** + * Locale variants for moox:demo (without --locales: first --languages entries). + * DemoLocalizationStep always merges these with CLI locales so entry seeders find rows. + */ + 'default_locales' => [ + 'cs_CZ', + 'en_US', + 'de_DE', + 'pl_PL', + ], + + /** + * Explicit package slug order before topological sort (earlier = higher priority). + */ + 'seeder_order' => [ + 'data', + 'localization', + 'media', + 'user', + 'attribute', + 'tag', + 'category', + 'draft', + 'product', + 'item', + 'news', + 'page', + 'press', + ], + + /** + * Slugs to skip when running package entry seeders. + */ + 'seeder_skip' => [ + 'demo', + 'core', + 'devlink', + 'devtools', + 'skeleton', + 'build', + 'monorepo', + ], + + /** + * Basenames of seeders to never run directly (called by a parent seeder). + */ + 'nested_seeder_basenames' => [ + 'StaticCountrySeeder', + 'StaticLanguageSeeder', + 'StaticCurrencySeeder', + 'StaticTimezoneSeeder', + 'StaticLocaleSeeder', + 'StaticCountriesStaticTimezonesSeeder', + 'StaticCountriesStaticCurrenciesSeeder', + ], + + 'demo_user' => [ + 'enabled' => true, + 'name' => 'Moox Demo', + 'email' => 'demo@moox.org', + 'password' => 'password', + ], + + 'media' => [ + 'disk' => 'public', + 'directory' => 'demo', + 'collection' => 'default', + /** Resolved to assets/images/users when null (see DemoRunner). */ + 'users_path' => null, + /** + * Static demo asset pools (under packages/demo/resources/demo/assets/). + * Used by seeders once DemoAssetCatalog / import step is wired (Phase 2.5). + */ + 'assets_path' => null, + 'products_path' => null, + 'pdf_path' => null, + 'documents_path' => null, + 'audio_path' => null, + 'videos_path' => null, + ], + +]; diff --git a/packages/demo/docs/SEEDERS.md b/packages/demo/docs/SEEDERS.md new file mode 100644 index 0000000000..d04ea87603 --- /dev/null +++ b/packages/demo/docs/SEEDERS.md @@ -0,0 +1,616 @@ +# Seeder mit `moox/demo` — Anleitung + +Diese Anleitung beschreibt, wie Moox-Pakete **Eintrags-Seeder** schreiben und registrieren, damit sie von `php artisan moox:demo` ausgeführt werden — und wie sie **ohne** Demo-Paket weiterhin per `db:seed` laufen. Als Referenz dienen die produktiven Implementierungen in **`moox/user`** (`UserSeeder`) und **`moox/category`** (`CategorySeeder`). + +**Agent-Skill (vollständige Regeln, Checklisten):** [moox-seeders/SKILL.md](../../.cursor/skills/moox-seeders/SKILL.md) · [examples.md](../../.cursor/skills/moox-seeders/examples.md) + +--- + +## Inhaltsverzeichnis + +1. [Überblick](#überblick) +2. [Registrierung im Paket](#registrierung-im-paket) +3. [Ablauf von `moox:demo`](#ablauf-von-mooxdemo) +4. [Referenz: `UserSeeder`](#referenz-userseeder-mooxuser) +5. [Referenz: `CategorySeeder`](#referenz-categoryseeder-mooxcategory) +6. [Demo-Integration (API)](#demo-integration-api) +7. [Eigenen Seeder anlegen](#eigenen-seeder-anlegen) +8. [Standalone vs. Demo-Pipeline](#standalone-vs-demo-pipeline) +9. [Konfiguration](#konfiguration) +10. [Locale-sprachiger Demo-Text](#locale-sprachiger-demo-text) +11. [Fehlersuche](#fehlersuche) + +--- + +## Überblick + +| Begriff | Bedeutung | +|--------|-----------| +| **Eintrags-Seeder** | Genau **eine** Seeder-Klasse pro Moox-Paket, die `moox:demo` aufruft (nicht jede `Static*Seeder`-Datei einzeln). | +| **Nested Seeder** | Werden nur von einem Parent aufgerufen (z. B. `StaticLanguageSeeder` durch `DataSeeder`) und erscheinen in `demo.nested_seeder_basenames`. | +| **Dataset** | CLI-Option `--dataset=small\|medium\|large\|huge` — steuert u. a. Zusatzdaten und Factory-Läufe (`demo.dataset_count`). | +| **Demo-Assets** | Optionale Medien aus `packages/demo/resources/demo/assets/` (Avatare, Produktbilder, …), nur wenn `moox/demo` aktiv seedet. | + +`moox:demo` führt **keinen** blinden `DatabaseSeeder` über alle Pakete aus. Es ermittelt installierte `moox/*`-Pakete, liest `composer.json` → `extra.moox.install.seed`, sortiert nach Composer-Abhängigkeiten und `config/demo.php` → `seeder_order`, und ruft pro Paket **eine** Klasse auf. + +--- + +## Registrierung im Paket + +### 1. Seeder-Klasse und PSR-4 + +Die Klasse liegt unter `database/seeders/` und wird im `composer.json` des Pakets autoloaded: + +```json +"autoload": { + "psr-4": { + "Moox\\User\\Database\\Seeders\\": "database/seeders" + } +} +``` + +### 2. Eintrag in `extra.moox.install.seed` + +**`moox/user`:** + +```json +"moox": { + "install": { + "seed": "database/seeders/UserSeeder.php" + } +} +``` + +**`moox/category`:** + +```json +"moox": { + "install": { + "seed": "database/seeders/CategorySeeder.php", + "auto_entities": { + "Category": true + } + } +} +``` + +- `seed` — Pfad relativ zum Paketroot; daraus wird der Klassenname abgeleitet (`UserSeeder` → `Moox\User\Database\Seeders\UserSeeder`). +- `auto_entities` — betrifft einen **zusätzlichen** Schritt nach den Seedern (Factory-Lauf mit `--dataset`), nicht den Entry-Seeder selbst. + +### 3. Paket nicht von der Demo ausschließen + +In `config/demo.php` stehen Slugs in `seeder_skip` (z. B. `demo`, `core`), die **nie** automatisch laufen. Eigene Pakete dort nicht eintragen, wenn sie Teil der Demo sein sollen. + +### 4. Abhängigkeiten in `composer.json` + +`moox/category` verlangt u. a. `moox/localization`. Die Demo sortiert Pakete topologisch: `user` vor `category`, wenn `category` von `user` abhängt (direkt oder transitiv). Zusätzlich kann `seeder_order` die Reihenfolge bei gleicher Tiefe festziehen. + +--- + +## Ablauf von `moox:demo` + +```mermaid +flowchart TD + A["moox:demo"] --> B{"--fresh?"} + B -->|ja| C["migrate:fresh"] + B -->|nein| D["Static data"] + C --> D + D --> E["DataSeeder · moox/data"] + E --> F["Localizations\n(--locales + default_locales)"] + F --> G["Demo media\n(Storage-Kopie)"] + G --> H["UserSeeder · moox/user"] + H --> I["Demo-User\n(falls konfiguriert)"] + I --> J["Weitere Entry-Seeders\n(z. B. CategorySeeder)"] + J --> K["Factory entities\n(--dataset, auto_entities)"] +``` + +Wichtig für eure Seeder: + +| Phase | Relevanz für `UserSeeder` / `CategorySeeder` | +|-------|-----------------------------------------------| +| `DataSeeder` | `static_languages`, Länder, Währungen — Voraussetzung für Localizations. | +| Localizations | `CategorySeeder::LOCALES` müssen als `locale_variant` existieren; `default_locales` in `demo.php` wird mit CLI-Locales gemerged. | +| Demo media | Kopiert Dateien aus `resources/demo/media/`; Mediathek-Befüllung für Kategorien kommt oft aus `media`-Tabelle (vorher importieren oder `moox/media` nutzen). | +| `UserSeeder` | Läuft **eigenständig** in der User-Phase (vor den übrigen Paket-Seedern). | +| `CategorySeeder` | Läuft in „Package seeders“, wenn `moox/category` installiert ist — **nach** User und Localizations. | + +Während des Laufs setzt `DemoRunner` u. a.: + +- `config('demo.runtime.seeding')` → `true` +- `config('demo.runtime.skip_media')` → entspricht `--skip-media` +- `config('demo.dataset_count')` → Wert aus `--dataset` +- `config('demo.media.users_path')` → z. B. `…/demo/resources/demo/assets/images/users` + +--- + +## Referenz: `UserSeeder` (`moox/user`) + +Datei: `packages/user/database/seeders/UserSeeder.php` + +### Ziele + +- Feste Demo-Accounts für Login und Filament-Tests. +- Zusätzliche Benutzer in der Größe des gewählten **Datasets**. +- Optionale **Avatare** aus Demo-Bildern in die Mediathek (`moox/media`). + +### Struktur `run()` + +Das Muster für alle demo-fähigen Seeder: + +```php +public function run(): void +{ + $this->seed(); + + if (class_exists(\Moox\Demo\Seeding\RunsMooxDemoAssets::class)) { + \Moox\Demo\Seeding\RunsMooxDemoAssets::invoke($this); + } +} +``` + +- **`seed()`** — Kernlogik; funktioniert auch ohne installiertes `moox/demo`. +- **`RunsMooxDemoAssets::invoke($this)`** — ruft optional `seedDemoAssets()` auf (nur wenn Demo seedet und `--skip-media` nicht gesetzt ist). + +### Feste Definitionen: `SEED_USER_EMAILS` + +```php +public const SEED_USER_EMAILS = [ + 'reinhold.jesse@heco.de', + 'admin@moox.org', + 'editor@moox.org', +]; +``` + +Anzeigenamen kommen per Faker (`de_DE` für `firstName`/`lastName`), damit Autor-Labels in `?lang=de_DE` deutsch wirken. Passwörter: `passwordForEmail()` (z. B. `123456789` für `heco.de`, sonst `password`). + +Diese Accounts werden **bei jedem Lauf** neu angelegt (nach `purgeDemoUsers()`). Feste E-Mails, `@moox.org`-Accounts und `demo-user-*@moox.org` werden vor dem Seed gelöscht, damit Wiederholungen idempotent bleiben. + +### Dataset: Anzahl Zusatz-Benutzer + +```php +$extraCount = $this->resolveExtraUserCount(); +// … +private function resolveExtraUserCount(): int +{ + $smallDefault = (int) (config('demo.dataset_sizes.small') ?? 100); + + if (class_exists(\Moox\Demo\Seeding\SeedingConfig::class)) { + return \Moox\Demo\Seeding\SeedingConfig::resolveCount('user', $smallDefault); + } + + return $smallDefault; +} +``` + +| Aufruf | Zusatz-User (Beispiel `small` = 100) | +|--------|--------------------------------------| +| `moox:demo` | `3` Standard + `100` → `demo-user-001@moox.org` … | +| `moox:demo --dataset=medium` | `3` + `1000` | +| `db:seed --class=UserSeeder` ohne Demo | `3` + `100` (Fallback `dataset_sizes.small`) | + +### Konsolen-Ausgabe mit `SeedOutput` + +Wenn `moox:demo` läuft, ist `SeedOutput` an die Demo-Konsole gebunden: + +```php +\Moox\Demo\Seeding\SeedOutput::created("User {$email}"); +\Moox\Demo\Seeding\SeedOutput::detail('…'); +\Moox\Demo\Seeding\SeedOutput::progressBar($extraCount, 'Demo users'); +``` + +Ohne Demo: Fallback auf `$this->command?->info()` — gleicher Seeder, normale Artisan-Ausgabe. + +### Demo-Medien: `seedDemoAssets()` + +Protected Methode, **nur** über `RunsMooxDemoAssets` aufgerufen: + +1. Prüft `ImportDemoMediaToMediathek` und `Media`-Model. +2. Liest Bilder aus `config('demo.media.users_path')` (vom Demo-Runner auf `assets/images/users` gesetzt). +3. Importiert in die Mediathek, verknüpft `media_usables`, setzt `avatar_url` am User. + +Ohne `moox/media` oder ohne Bilder im Ordner: Seeder bricht Medien-Schritt still ab (Warnung nur mit Command). + +### Abhängigkeiten + +| Paket | Zweck | +|-------|--------| +| `moox/core` | Basis | +| `moox/media` | Avatare (optional, aber für `seedDemoAssets` nötig) | + +--- + +## Referenz: `CategorySeeder` (`moox/category`) + +Datei: `packages/category/database/seeders/CategorySeeder.php` + +### Ziele + +- Verschachtelter Kategoriebaum (Nested Set, max. Tiefe 4). +- Vier Locales mit **locale-sprachigem** Fließtext (Faker `realText*`, siehe [Locale-sprachiger Demo-Text](#locale-sprachiger-demo-text)). +- Zufällige Übersetzungs-Status (`published`, `draft`, `scheduled`, …). +- Verknüpfung mit **bestehenden** Mediathek-Einträgen (`media_usables`), nicht Upload im Seeder. + +### Voraussetzungen (Pflicht) + +Vor `CategorySeeder` müssen existieren: + +1. **Mindestens ein User** — `requireDemoAuthor()`; sonst `error` und Abbruch (zuerst `UserSeeder`). +2. **Localizations** für alle Locales aus `$this->locales()` (Fallback-Konstante `LOCALES`, sonst `config/demo.php`): + + ```php + public const LOCALES = ['cs_CZ', 'en_US', 'de_DE', 'pl_PL']; + // In seed(): foreach ($this->locales() as $locale) { … } + ``` + + `moox:demo` legt diese über `default_locales` plus `--locales` an. + +3. **Optional, empfohlen:** Einträge in `media` (Bilder). Ohne Medien: Warnung, Kategorien ohne Bild. + +### Seed-Batch und Wiederholbarkeit + +```php +public const SEED_BATCH = 'category_seeder_v1'; +``` + +Jede Kategorie erhält `basedata.seed_batch` und `basedata.seed_index`. Vor jedem Lauf ruft `purgeSeededCategories()` alle Kategorien mit `basedata->seed_batch = category_seeder_v1` ab (inkl. Übersetzungen), damit `moox:demo` wiederholbar bleibt und **keine alten Lorem-/Legacy-Texte** in der DB bleiben. + +### Anzahl Kategorien + +```php +private function resolvedCount(): int +{ + if ($this->count !== null) { + return max(1, min(5000, $this->count)); + } + + $fromEnv = env('CATEGORY_MOCK_COUNT'); + if ($fromEnv !== null && $fromEnv !== '') { + return max(1, min(5000, (int) $fromEnv)); + } + + return 100; +} +``` + +| Steuerung | Beispiel | +|-----------|----------| +| `moox:demo --dataset=medium` | `SeedingConfig::resolveCount('category', 100)` | +| Konstruktor | `new CategorySeeder(count: 250)` (bei manuellem Resolve aus Container) | +| `.env` | `CATEGORY_MOCK_COUNT=500` (Fallback ohne Demo) | +| Standard | `100` | + +### Datenmodell (Kurz) + +- **Traits:** `FormatsFakerLocaleText`, `ReportsMooxSeederProgress`, `LoadsImageMediaPool`, `RunsMooxDemoAssets` (optional). +- **Baum:** `buildParentIndexMap($total)` — verschachtelte `parent_id` im selben Lauf. +- **Übersetzungen:** pro Locale `fakerLocaleTitle`, `fakerLocaleText` (Preset `description`), `markdownContentFromLocale` für `content`; `assignTranslationAuthor`. +- **Medien:** `AttachExistingMedia::attach(..., $locale)` mit Pivot-Locale `de_DE` (`primaryMediaLocale()`), Wahrscheinlichkeit 85 %, wenn Bilder in der DB sind. +- Abschluss: `Category::fixTree()` und Aktualisierung von `count` (Kinderzahl). + +### `run()`-Muster + +Identisch zu `UserSeeder`: zuerst `seed()`, dann optional `RunsMooxDemoAssets`. `CategorySeeder` definiert aktuell **kein** `seedDemoAssets()` — der zweite Schritt ist ein No-Op. + +### Abhängigkeiten + +| Paket | Zweck | +|-------|--------| +| `moox/core` | Draft/Translation-Basisklassen | +| `moox/localization` | `Localization`-Model | +| `moox/user` | Autor der Übersetzungen | +| `moox/media` | `media` / `media_usables` (empfohlen) | + +--- + +## Demo-Integration (API) + +Alle Klassen liegen unter `Moox\Demo\Seeding\` und sind **weich** gekoppelt: `class_exists()` im Paket-Seeder, kein harter `require` von `moox/demo` in `composer.json` des Fachpakets. + +### `RunsMooxDemoAssets` + +```php +RunsMooxDemoAssets::invoke($this); +``` + +Ruft `protected function seedDemoAssets(): void` auf, wenn: + +- `config('demo.runtime.seeding') === true` +- `config('demo.runtime.skip_media') !== true` +- die Methode existiert + +### `DemoAssetGate` + +```php +DemoAssetGate::enabled(); // seeding && !skip_media +``` + +### `SeedingConfig` + +```php +SeedingConfig::resolveCount('user', $default); +``` + +Liefert `config('demo.dataset_count')`, wenn Demo seedet — sonst `$default`. Slug-Parameter ist für spätere per-Paket-Overrides vorgesehen; aktuell global ein Wert. + +### `SeedOutput` + +Nur während `moox:demo` gebunden. In eigenen Seedern: + +```php +private function hasSeedOutput(): bool +{ + return class_exists(\Moox\Demo\Seeding\SeedOutput::class) + && \Moox\Demo\Seeding\SeedOutput::isBound(); +} +``` + +### `ImportDemoMediaToMediathek` + +- `listImagePaths($dir, $limit)` — sortierte JPG/PNG/WebP-Liste +- `importFromPath($path, $collectionId)` — Dedupe per SHA-256 in `custom_properties.file_hash` +- `avatarUrlFromMedia($media)` — JSON für `User.avatar_url` + +### Shared Seeding-Traits (`Moox\Demo\Seeding`) + +| Trait | Zweck | +|-------|--------| +| `FormatsFakerLocaleText` | Locale-Lock-Fließtext via Faker `realTextBetween` (siehe unten) | +| `ReportsMooxSeederProgress` | `reportDetail`, `reportCreated`, `assertRequiredLocalizations`, `requireDemoAuthor` | +| `LoadsImageMediaPool` | Medien-Pool für `media_usables` | + +--- + +## Locale-sprachiger Demo-Text + +**Regel:** Zeile `locale = de_DE` → alle sichtbaren Textfelder **deutsch** (gleiches für `en_US`, `cs_CZ`, `pl_PL`). Mehrere Locale-Zeilen pro Entity sind ok; **eine Zeile = eine Sprache**. + +### Warum nicht `sentence()` / `paragraph()`? + +Faker-Lorem liefert bei `Factory::create('de_DE')` oft **Pseudo-Latein** (`Doloremque`, `Voluptat`, …). Nur der **Text-Provider** (`realText`, `realTextBetween`) erzeugt sprachige Auszüge. Details und Anti-Patterns: [Cursor-Skill `moox-seeders`](../../../.cursor/skills/moox-seeders/SKILL.md) (Abschnitt Locale-Lock). + +### API (`FormatsFakerLocaleText`) + +```php +use Moox\Demo\Seeding\FormatsFakerLocaleText; + +class ProductSeeder extends Seeder +{ + use FormatsFakerLocaleText; + + // In foreach (self::LOCALES as $locale): + $localeFaker = $this->fakerForLocale($locale); + + $translation->title = $this->fakerLocaleTitle($locale, $localeFaker, 'title'); + $translation->description = $this->fakerLocaleText($locale, $localeFaker, preset: 'description'); + $translation->content = $this->markdownContentFromLocale($locale, $localeFaker, 3, 6); + // Kurztext mit fester DB-Grenze: + // $translation->excerpt = $this->fakerLocaleText($locale, $localeFaker, 80, 180, limit: 180); +} +``` + +| Methode | Zweck | +|---------|--------| +| `fakerLocaleTitle()` | Titel (Preset `title` / `tag_title`) | +| `fakerLocaleText()` | Fließtext; Presets: `description`, `body`, `content`, … oder eigene `minChars`/`maxChars` | +| `fakerLocaleSentence()` | Erster Satz aus einem Textblock | +| `fakerLocaleParagraphs()` | Liste von Absätzen | +| `markdownContentFromLocale()` | `##` Überschrift + Absätze | +| `formatFakerWords()` / `formatFakerSentence()` | BC-Wrapper → intern `fakerLocaleTitle` | + +**Presets** (Zeichen min/max, intern `random_int`): `title`, `tag_title`, `subtitle`, `excerpt`, `description`, `body`, `content`. + +**Erkennung:** `localeSupportsRealText()` probeert `realTextBetween` (nicht `method_exists`). Fehlt `realText` für Moox-Locales → `RuntimeException`, kein stilles Lorem. + +Referenz-Seeder mit diesem Muster: `CategorySeeder`, `ProductSeeder`, `TagSeeder`, `DraftSeeder`, `ItemSeeder`, `RecordSeeder`. + +### Prüfung nach Seed + +```sql +SELECT locale, title, LEFT(description, 60), LEFT(content, 60) +FROM category_translations +WHERE locale = 'de_DE' +ORDER BY id DESC +LIMIT 5; +``` + +In Filament (`?lang=de_DE`) müssen Formularwerte der DB-Zeile `de_DE` entsprechen (UI-Fallback auf andere Locales ist ein separates Thema). + +--- + +## Eigenen Seeder anlegen + +Checkliste am Beispiel eines fiktiven Pakets `moox/shop`: + +### 1. Klasse erstellen + +`packages/shop/database/seeders/ShopSeeder.php` + +```php +namespace Moox\Shop\Database\Seeders; + +use Faker\Factory as FakerFactory; +use Faker\Generator; +use Illuminate\Database\Seeder; +use Moox\Demo\Seeding\FormatsFakerLocaleText; +use Moox\Demo\Seeding\ReportsMooxSeederProgress; + +class ShopSeeder extends Seeder +{ + use FormatsFakerLocaleText; + use ReportsMooxSeederProgress; + + /** Fallback when moox/demo is not installed. */ + public const LOCALES = ['cs_CZ', 'en_US', 'de_DE', 'pl_PL']; + + public function run(): void + { + $this->seed(); + + if (class_exists(\Moox\Demo\Seeding\RunsMooxDemoAssets::class)) { + \Moox\Demo\Seeding\RunsMooxDemoAssets::invoke($this); + } + } + + protected function seed(): void + { + $count = 10; + + if (class_exists(\Moox\Demo\Seeding\SeedingConfig::class)) { + $count = \Moox\Demo\Seeding\SeedingConfig::resolveCount( + 'shop', + (int) config('demo.dataset_sizes.small', 100) + ); + } + + foreach ($this->locales() as $locale) { + $localeFaker = $this->fakerForLocale($locale); + // … translateOrNew($locale), fakerLocaleText / fakerLocaleTitle … + } + } + + private function fakerForLocale(string $locale): Generator + { + static $cache = []; + + return $cache[$locale] ??= FakerFactory::create($locale); + } + + protected function seedDemoAssets(): void + { + // Optional: Medien aus config('demo.media.products_path') o. ä. + } +} +``` + +### 2. `composer.json` registrieren + +```json +"extra": { + "moox": { + "install": { + "seed": "database/seeders/ShopSeeder.php" + } + } +} +``` + +### 3. `config/demo.php` anpassen (Host-App) + +```php +'seeder_order' => [ + // … + 'category', + 'shop', // nach category, wenn shop von category abhängt +], +``` + +### 4. Voraussetzungen dokumentieren und prüfen + +Wie `CategorySeeder`: früh `return` mit `$this->command?->error('…')`, wenn Tabellen oder Fremddaten fehlen. + +### 5. Konstanten für Demo-Daten + +| Muster (`UserSeeder`) | Muster (`CategorySeeder`) | +|----------------------|---------------------------| +| `SEED_USER_EMAILS` — feste Logins | `LOCALES`, `SEED_BATCH`, Faker pro Locale | +| `purgeDemoUsers()` — idempotent | `purgeSeededCategories()` — idempotent | +| `DEMO_EMAIL_DOMAIN` | `FormatsFakerLocaleText` für Übersetzungen | + +### 6. Keine harte Abhängigkeit auf `moox/demo` + +Fachpakete bleiben in `composer.json` ohne `moox/demo`. Nur optionale `class_exists`-Aufrufe. + +--- + +## Standalone vs. Demo-Pipeline + +### Nur Demo-Pipeline (empfohlen für lokale Moox-App) + +```bash +php artisan moox:demo +php artisan moox:demo --locales=de_DE,en_US,cs_CZ,pl_PL --dataset=small +php artisan moox:demo --fresh --dataset=medium +php artisan moox:demo --skip-factories --skip-media +php artisan moox:demo --skip-seeders # nur Static + Localization + Factories +``` + +### Einzelne Seeder (Debugging) + +```bash +php artisan db:seed --class=Moox\\User\\Database\\Seeders\\UserSeeder --force +php artisan db:seed --class=Moox\\Category\\Database\\Seeders\\CategorySeeder --force +``` + +Reihenfolge manuell einhalten: + +1. `DataSeeder` (oder `moox:demo` bis nach Static data) +2. Localizations (`cs_CZ`, `en_US`, `de_DE`, `pl_PL` für Category) +3. Optional Mediathek befüllen +4. `UserSeeder` +5. `CategorySeeder` + +Mit `CATEGORY_MOCK_COUNT=50` in `.env` oder angepasstem Konstruktor die Kategoriezahl steuern. + +--- + +## Konfiguration + +Nach `php artisan vendor:publish --tag=demo-config` in der Host-App: + +| Schlüssel | Wirkung auf Seeder | +|-----------|-------------------| +| `dataset_sizes` | Grenzen für `SeedingConfig` / Factory | +| `default_locales` | Wird mit CLI-Locales gemerged — **wichtig für CategorySeeder** | +| `seeder_order` | Priorität bei topological sort | +| `seeder_skip` | Paket-Slug wird übersprungen | +| `nested_seeder_basenames` | Nur Parent-Seeder (z. B. `DataSeeder`) | +| `demo_user` | Zusätzlicher User in Demo-Phase (zusätzlich zu `UserSeeder`) | +| `media.users_path` | Avatare für `UserSeeder::seedDemoAssets` | +| `media.disk` / `media.directory` | Storage-Kopie in `DemoMediaStep` | + +Runtime (nur während `moox:demo`, nicht in Config-Datei publiziert): + +- `demo.runtime.seeding` +- `demo.runtime.skip_media` +- `demo.dataset_count` + +--- + +## Fehlersuche + +| Symptom | Ursache | Maßnahme | +|---------|---------|----------| +| Paket-Seeder wird nicht ausgeführt | Nicht installiert, in `seeder_skip`, oder kein `extra.moox.install.seed` | `composer require`, `composer.json` prüfen | +| `CategorySeeder`: No user found | `UserSeeder` nicht gelaufen | `moox:demo` komplett oder zuerst `UserSeeder` | +| Missing `localizations` rows | Locales fehlen | `moox:demo` mit `default_locales` oder LocalizationSeeder | +| Kategorien ohne Bilder | Leere `media`-Tabelle | Mediathek befüllen oder Demo-Medien importieren | +| Avatare fehlen | `--skip-media`, kein `moox/media`, leerer `users_path` | Option weglassen, Media-Paket, Bilder unter `assets/images/users` | +| Zu viele/wenige User | Dataset | `--dataset=`; standalone: Default 100 Extras | +| Zu viele/wenige Kategorien | Dataset / Env | `--dataset=` oder `CATEGORY_MOCK_COUNT` | +| `de_DE` mit lateinischen Titeln | Alter DB-Stand oder Lorem-Formatter | `purgeSeededCategories` + neu seeden; nur `fakerLocale*` / `realText*` nutzen | +| Seeder-Klasse not found | Falscher Namespace / Autoload | PSR-4 in `composer.json`, `composer dump-autoload` | + +--- + +## Kurzvergleich der Referenz-Seeder + +| Aspekt | `UserSeeder` | `CategorySeeder` | +|--------|--------------|------------------| +| Registrierung | `extra.moox.install.seed` | gleich | +| Dataset-Anbindung | `SeedingConfig::resolveCount('user')` | `SeedingConfig::resolveCount('category')` | +| Idempotenz | `purgeDemoUsers()` | `purgeSeededCategories()` | +| Locale-Text | Faker `de_DE` Namen (Autor) | `FormatsFakerLocaleText` / `realText*` | +| Demo-Medien | `seedDemoAssets()` (Avatare) | vorhandene `media`-Zeilen, Pivot `de_DE` | +| Locales | — | feste `LOCALES` (4) | +| `RunsMooxDemoAssets` | ja | ja (ohne `seedDemoAssets`) | + +--- + +## Weiterführend + +- [Cursor-Skill `moox-seeders`](../../../.cursor/skills/moox-seeders/SKILL.md) — Locale-Lock, Relationen, Performance, Terminal-UX +- [Skill-Beispiele `examples.md`](../../../.cursor/skills/moox-seeders/examples.md) — Minimal-Seeder mit `fakerLocaleText` +- [README des Demo-Pakets](../README.md) — Installation, CLI-Optionen, Medienordner +- [MEDIA_SOURCES.md](../resources/demo/assets/MEDIA_SOURCES.md) — Lizenzen der Demo-Assets +- [Faker Formatters](https://fakerphp.org/formatters/) / [de_DE](https://fakerphp.org/locales/de_DE/) — `realText*` vs. Lorem +- `php artisan moox:demo -vv` — ausführliche Reihenfolge und übersprungene Schritte diff --git a/packages/demo/resources/demo/assets/MEDIA_SOURCES.md b/packages/demo/resources/demo/assets/MEDIA_SOURCES.md new file mode 100644 index 0000000000..c2d4328415 --- /dev/null +++ b/packages/demo/resources/demo/assets/MEDIA_SOURCES.md @@ -0,0 +1,75 @@ +# Demo asset sources + +Static files under `resources/demo/assets/` are bundled with `moox/demo` for offline seeding. +They were fetched once for maintainers; **`moox:demo` does not download from the internet.** + +Fetched: 2026-05-26 + +## Layout + +| Path | Purpose | Count (approx.) | +|------|---------|-----------------| +| `images/products/` | Product / category style photos | 36 | +| `images/users/` | User avatar photos | 222 | +| `files/pdf/` | PDF documents | 4 | +| `files/documents/` | TXT, DOCX, XLSX | 3 | +| `files/audio/` | MP3 sample | 1 | +| `videos/short/` | Short MP4/WebM clips | 5 | + +## Sources and licenses + +### User avatars (`images/users/`) + +- **Source:** Bundled demo portraits (legacy pool from `resources/demo/media/users/`, relocated 2026-05-27). +- **License:** Demo use only; replace with your own avatars in production. + +### Product images (`images/products/`) + +- **Source:** [Lorem Picsum](https://picsum.photos/) — `https://picsum.photos/id/{id}/800/800` +- **License:** Photos from [Unsplash](https://unsplash.com/license) via Picsum; use for demos only, not as your own product photography in production marketing without checking each image. + +### PDF (`files/pdf/`) + +| File | URL | +|------|-----| +| `sample-dummy.pdf` | https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf | +| `sample-1.pdf` … `sample-3.pdf` | https://filesamples.com/samples/document/pdf/sample{1,2,3}.pdf | + +### Documents (`files/documents/`) + +| File | URL | +|------|-----| +| `sample.txt` | https://filesamples.com/samples/document/txt/sample1.txt | +| `sample.docx` | https://filesamples.com/samples/document/docx/sample1.docx | +| `sample.xlsx` | https://filesamples.com/samples/document/xlsx/sample1.xlsx | + +### Audio (`files/audio/`) + +| File | URL | +|------|-----| +| `sample.mp3` | https://filesamples.com/samples/audio/mp3/sample1.mp3 | + +### Videos (`videos/short/`) + +| File | URL | Notes | +|------|-----|-------| +| `sample-5s.mp4` | https://download.samplelib.com/mp4/sample-5s.mp4 | ~2.7 MB | +| `sample-10s.mp4` | https://download.samplelib.com/mp4/sample-10s.mp4 | ~5.2 MB | +| `flower.webm` | https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm | CC0 (MDN) | +| `big-buck-bunny-360.mp4` | https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/360/Big_Buck_Bunny_360_10s_1MB.mp4 | Blender Foundation | +| `sample-mp4.mp4` | https://www.learningcontainer.com/wp-content/uploads/2020/05/sample-mp4-file.mp4 | ~10 MB | + +**Total video size:** ~20 MB — consider Git LFS if the repo grows further (see demo package plan Phase 2.5). + +## Re-download (maintainers) + +From the repo root, example for product images (PowerShell): + +```powershell +$dir = "packages/demo/resources/demo/assets/images/products" +1..35 | ForEach-Object { + Invoke-WebRequest -Uri "https://picsum.photos/id/$_/800/800" -OutFile "$dir/product-{0:D3}.jpg" -f $_ +} +``` + +See URLs in the tables above for other types. diff --git a/packages/demo/resources/demo/assets/files/audio/sample.mp3 b/packages/demo/resources/demo/assets/files/audio/sample.mp3 new file mode 100644 index 0000000000..d134f76daf Binary files /dev/null and b/packages/demo/resources/demo/assets/files/audio/sample.mp3 differ diff --git a/packages/demo/resources/demo/assets/files/documents/sample.docx b/packages/demo/resources/demo/assets/files/documents/sample.docx new file mode 100644 index 0000000000..b172c4395d Binary files /dev/null and b/packages/demo/resources/demo/assets/files/documents/sample.docx differ diff --git a/packages/demo/resources/demo/assets/files/documents/sample.txt b/packages/demo/resources/demo/assets/files/documents/sample.txt new file mode 100644 index 0000000000..feab84d4a4 --- /dev/null +++ b/packages/demo/resources/demo/assets/files/documents/sample.txt @@ -0,0 +1,4 @@ +Utilitatis causa amicitia est quaesita. +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Collatio igitur ista te nihil iuvat. Honesta oratio, Socratica, Platonis etiam. Primum in nostrane potestate est, quid meminerimus? Duo Reges: constructio interrete. Quid, si etiam iucunda memoria est praeteritorum malorum? Si quidem, inquit, tollerem, sed relinquo. An nisi populari fama? + +Quamquam id quidem licebit iis existimare, qui legerint. Summum a vobis bonum voluptas dicitur. At hoc in eo M. Refert tamen, quo modo. Quid sequatur, quid repugnet, vident. Iam id ipsum absurdum, maximum malum neglegi. \ No newline at end of file diff --git a/packages/demo/resources/demo/assets/files/documents/sample.xlsx b/packages/demo/resources/demo/assets/files/documents/sample.xlsx new file mode 100644 index 0000000000..04ac8aba5c Binary files /dev/null and b/packages/demo/resources/demo/assets/files/documents/sample.xlsx differ diff --git a/packages/demo/resources/demo/assets/files/pdf/sample-1.pdf b/packages/demo/resources/demo/assets/files/pdf/sample-1.pdf new file mode 100644 index 0000000000..93db6a1a9d Binary files /dev/null and b/packages/demo/resources/demo/assets/files/pdf/sample-1.pdf differ diff --git a/packages/demo/resources/demo/assets/files/pdf/sample-2.pdf b/packages/demo/resources/demo/assets/files/pdf/sample-2.pdf new file mode 100644 index 0000000000..72d0d21d38 Binary files /dev/null and b/packages/demo/resources/demo/assets/files/pdf/sample-2.pdf differ diff --git a/packages/demo/resources/demo/assets/files/pdf/sample-3.pdf b/packages/demo/resources/demo/assets/files/pdf/sample-3.pdf new file mode 100644 index 0000000000..28d84bfc51 Binary files /dev/null and b/packages/demo/resources/demo/assets/files/pdf/sample-3.pdf differ diff --git a/packages/demo/resources/demo/assets/files/pdf/sample-dummy.pdf b/packages/demo/resources/demo/assets/files/pdf/sample-dummy.pdf new file mode 100644 index 0000000000..774c2ea70c Binary files /dev/null and b/packages/demo/resources/demo/assets/files/pdf/sample-dummy.pdf differ diff --git a/packages/demo/resources/demo/assets/images/products/product-001.jpg b/packages/demo/resources/demo/assets/images/products/product-001.jpg new file mode 100644 index 0000000000..e1217534ce Binary files /dev/null and b/packages/demo/resources/demo/assets/images/products/product-001.jpg differ diff --git a/packages/demo/resources/demo/assets/images/products/product-002.jpg b/packages/demo/resources/demo/assets/images/products/product-002.jpg new file mode 100644 index 0000000000..7ebaa6feea Binary files /dev/null and b/packages/demo/resources/demo/assets/images/products/product-002.jpg differ diff --git a/packages/demo/resources/demo/assets/images/products/product-003.jpg b/packages/demo/resources/demo/assets/images/products/product-003.jpg new file mode 100644 index 0000000000..1a1445b2f1 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/products/product-003.jpg differ diff --git a/packages/demo/resources/demo/assets/images/products/product-004.jpg b/packages/demo/resources/demo/assets/images/products/product-004.jpg new file mode 100644 index 0000000000..e58b785227 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/products/product-004.jpg differ diff --git a/packages/demo/resources/demo/assets/images/products/product-005.jpg b/packages/demo/resources/demo/assets/images/products/product-005.jpg new file mode 100644 index 0000000000..d3529add16 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/products/product-005.jpg differ diff --git a/packages/demo/resources/demo/assets/images/products/product-006.jpg b/packages/demo/resources/demo/assets/images/products/product-006.jpg new file mode 100644 index 0000000000..5e53486ab3 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/products/product-006.jpg differ diff --git a/packages/demo/resources/demo/assets/images/products/product-007.jpg b/packages/demo/resources/demo/assets/images/products/product-007.jpg new file mode 100644 index 0000000000..0292732c33 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/products/product-007.jpg differ diff --git a/packages/demo/resources/demo/assets/images/products/product-008.jpg b/packages/demo/resources/demo/assets/images/products/product-008.jpg new file mode 100644 index 0000000000..30c575fc52 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/products/product-008.jpg differ diff --git a/packages/demo/resources/demo/assets/images/products/product-009.jpg b/packages/demo/resources/demo/assets/images/products/product-009.jpg new file mode 100644 index 0000000000..6d9453e81a Binary files /dev/null and b/packages/demo/resources/demo/assets/images/products/product-009.jpg differ diff --git a/packages/demo/resources/demo/assets/images/products/product-010.jpg b/packages/demo/resources/demo/assets/images/products/product-010.jpg new file mode 100644 index 0000000000..b8bc1c0f40 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/products/product-010.jpg differ diff --git a/packages/demo/resources/demo/assets/images/products/product-011.jpg b/packages/demo/resources/demo/assets/images/products/product-011.jpg new file mode 100644 index 0000000000..d45c5a139e Binary files /dev/null and b/packages/demo/resources/demo/assets/images/products/product-011.jpg differ diff --git a/packages/demo/resources/demo/assets/images/products/product-012.jpg b/packages/demo/resources/demo/assets/images/products/product-012.jpg new file mode 100644 index 0000000000..3a315f2471 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/products/product-012.jpg differ diff --git a/packages/demo/resources/demo/assets/images/products/product-013.jpg b/packages/demo/resources/demo/assets/images/products/product-013.jpg new file mode 100644 index 0000000000..4ca0115f8d Binary files /dev/null and b/packages/demo/resources/demo/assets/images/products/product-013.jpg differ diff --git a/packages/demo/resources/demo/assets/images/products/product-014.jpg b/packages/demo/resources/demo/assets/images/products/product-014.jpg new file mode 100644 index 0000000000..cc55fd50ec Binary files /dev/null and b/packages/demo/resources/demo/assets/images/products/product-014.jpg differ diff --git a/packages/demo/resources/demo/assets/images/products/product-015.jpg b/packages/demo/resources/demo/assets/images/products/product-015.jpg new file mode 100644 index 0000000000..8e3a1b7561 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/products/product-015.jpg differ diff --git a/packages/demo/resources/demo/assets/images/products/product-016.jpg b/packages/demo/resources/demo/assets/images/products/product-016.jpg new file mode 100644 index 0000000000..d09cfdafeb Binary files /dev/null and b/packages/demo/resources/demo/assets/images/products/product-016.jpg differ diff --git a/packages/demo/resources/demo/assets/images/products/product-017.jpg b/packages/demo/resources/demo/assets/images/products/product-017.jpg new file mode 100644 index 0000000000..8881ec4325 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/products/product-017.jpg differ diff --git a/packages/demo/resources/demo/assets/images/products/product-018.jpg b/packages/demo/resources/demo/assets/images/products/product-018.jpg new file mode 100644 index 0000000000..7f48db052c Binary files /dev/null and b/packages/demo/resources/demo/assets/images/products/product-018.jpg differ diff --git a/packages/demo/resources/demo/assets/images/products/product-019.jpg b/packages/demo/resources/demo/assets/images/products/product-019.jpg new file mode 100644 index 0000000000..e3c0f8980b Binary files /dev/null and b/packages/demo/resources/demo/assets/images/products/product-019.jpg differ diff --git a/packages/demo/resources/demo/assets/images/products/product-020.jpg b/packages/demo/resources/demo/assets/images/products/product-020.jpg new file mode 100644 index 0000000000..c5c97ed6cb Binary files /dev/null and b/packages/demo/resources/demo/assets/images/products/product-020.jpg differ diff --git a/packages/demo/resources/demo/assets/images/products/product-021.jpg b/packages/demo/resources/demo/assets/images/products/product-021.jpg new file mode 100644 index 0000000000..916e85aa56 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/products/product-021.jpg differ diff --git a/packages/demo/resources/demo/assets/images/products/product-022.jpg b/packages/demo/resources/demo/assets/images/products/product-022.jpg new file mode 100644 index 0000000000..449f159223 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/products/product-022.jpg differ diff --git a/packages/demo/resources/demo/assets/images/products/product-023.jpg b/packages/demo/resources/demo/assets/images/products/product-023.jpg new file mode 100644 index 0000000000..6d1d29d7b9 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/products/product-023.jpg differ diff --git a/packages/demo/resources/demo/assets/images/products/product-024.jpg b/packages/demo/resources/demo/assets/images/products/product-024.jpg new file mode 100644 index 0000000000..b344ba5153 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/products/product-024.jpg differ diff --git a/packages/demo/resources/demo/assets/images/products/product-025.jpg b/packages/demo/resources/demo/assets/images/products/product-025.jpg new file mode 100644 index 0000000000..9b39c8670a Binary files /dev/null and b/packages/demo/resources/demo/assets/images/products/product-025.jpg differ diff --git a/packages/demo/resources/demo/assets/images/products/product-026.jpg b/packages/demo/resources/demo/assets/images/products/product-026.jpg new file mode 100644 index 0000000000..d9ab14504f Binary files /dev/null and b/packages/demo/resources/demo/assets/images/products/product-026.jpg differ diff --git a/packages/demo/resources/demo/assets/images/products/product-027.jpg b/packages/demo/resources/demo/assets/images/products/product-027.jpg new file mode 100644 index 0000000000..7cc69af0d6 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/products/product-027.jpg differ diff --git a/packages/demo/resources/demo/assets/images/products/product-028.jpg b/packages/demo/resources/demo/assets/images/products/product-028.jpg new file mode 100644 index 0000000000..9a69ac718c Binary files /dev/null and b/packages/demo/resources/demo/assets/images/products/product-028.jpg differ diff --git a/packages/demo/resources/demo/assets/images/products/product-029.jpg b/packages/demo/resources/demo/assets/images/products/product-029.jpg new file mode 100644 index 0000000000..12e8f13b9b Binary files /dev/null and b/packages/demo/resources/demo/assets/images/products/product-029.jpg differ diff --git a/packages/demo/resources/demo/assets/images/products/product-030.jpg b/packages/demo/resources/demo/assets/images/products/product-030.jpg new file mode 100644 index 0000000000..1b8a9f1d5c Binary files /dev/null and b/packages/demo/resources/demo/assets/images/products/product-030.jpg differ diff --git a/packages/demo/resources/demo/assets/images/products/product-031.jpg b/packages/demo/resources/demo/assets/images/products/product-031.jpg new file mode 100644 index 0000000000..c5b241fe23 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/products/product-031.jpg differ diff --git a/packages/demo/resources/demo/assets/images/products/product-032.jpg b/packages/demo/resources/demo/assets/images/products/product-032.jpg new file mode 100644 index 0000000000..0f698f4f0f Binary files /dev/null and b/packages/demo/resources/demo/assets/images/products/product-032.jpg differ diff --git a/packages/demo/resources/demo/assets/images/products/product-033.jpg b/packages/demo/resources/demo/assets/images/products/product-033.jpg new file mode 100644 index 0000000000..a31ce8ecae Binary files /dev/null and b/packages/demo/resources/demo/assets/images/products/product-033.jpg differ diff --git a/packages/demo/resources/demo/assets/images/products/product-034.jpg b/packages/demo/resources/demo/assets/images/products/product-034.jpg new file mode 100644 index 0000000000..cea0bab052 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/products/product-034.jpg differ diff --git a/packages/demo/resources/demo/assets/images/products/product-035.jpg b/packages/demo/resources/demo/assets/images/products/product-035.jpg new file mode 100644 index 0000000000..69c791d8cf Binary files /dev/null and b/packages/demo/resources/demo/assets/images/products/product-035.jpg differ diff --git a/packages/demo/resources/demo/assets/images/products/product-036.webp b/packages/demo/resources/demo/assets/images/products/product-036.webp new file mode 100644 index 0000000000..49c5987a46 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/products/product-036.webp differ diff --git a/packages/demo/resources/demo/assets/images/users/1.jpg b/packages/demo/resources/demo/assets/images/users/1.jpg new file mode 100644 index 0000000000..332b92a652 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/1.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/10.jpg b/packages/demo/resources/demo/assets/images/users/10.jpg new file mode 100644 index 0000000000..7236974edd Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/10.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/100.jpg b/packages/demo/resources/demo/assets/images/users/100.jpg new file mode 100644 index 0000000000..9dd021f435 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/100.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/101.jpg b/packages/demo/resources/demo/assets/images/users/101.jpg new file mode 100644 index 0000000000..9c764704c6 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/101.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/102.jpg b/packages/demo/resources/demo/assets/images/users/102.jpg new file mode 100644 index 0000000000..b468b0dbf8 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/102.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/103.jpg b/packages/demo/resources/demo/assets/images/users/103.jpg new file mode 100644 index 0000000000..8469b8bd20 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/103.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/104.jpg b/packages/demo/resources/demo/assets/images/users/104.jpg new file mode 100644 index 0000000000..6da915abaf Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/104.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/105.jpg b/packages/demo/resources/demo/assets/images/users/105.jpg new file mode 100644 index 0000000000..1d11d19306 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/105.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/106.jpg b/packages/demo/resources/demo/assets/images/users/106.jpg new file mode 100644 index 0000000000..35c45c2ec1 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/106.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/107.jpg b/packages/demo/resources/demo/assets/images/users/107.jpg new file mode 100644 index 0000000000..c36dc9d0a7 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/107.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/108.jpg b/packages/demo/resources/demo/assets/images/users/108.jpg new file mode 100644 index 0000000000..23b5c145d8 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/108.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/109.jpg b/packages/demo/resources/demo/assets/images/users/109.jpg new file mode 100644 index 0000000000..8dab5f5192 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/109.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/11.jpg b/packages/demo/resources/demo/assets/images/users/11.jpg new file mode 100644 index 0000000000..dae4842d66 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/11.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/110.jpg b/packages/demo/resources/demo/assets/images/users/110.jpg new file mode 100644 index 0000000000..f60c7fb8fe Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/110.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/111.jpg b/packages/demo/resources/demo/assets/images/users/111.jpg new file mode 100644 index 0000000000..e846097c41 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/111.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/112.jpg b/packages/demo/resources/demo/assets/images/users/112.jpg new file mode 100644 index 0000000000..edbb218691 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/112.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/113.jpg b/packages/demo/resources/demo/assets/images/users/113.jpg new file mode 100644 index 0000000000..4cb5e5427d Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/113.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/114.jpg b/packages/demo/resources/demo/assets/images/users/114.jpg new file mode 100644 index 0000000000..c118ca8e0b Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/114.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/115.jpg b/packages/demo/resources/demo/assets/images/users/115.jpg new file mode 100644 index 0000000000..8cb68f205c Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/115.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/116.jpg b/packages/demo/resources/demo/assets/images/users/116.jpg new file mode 100644 index 0000000000..8fbf59397f Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/116.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/117.jpg b/packages/demo/resources/demo/assets/images/users/117.jpg new file mode 100644 index 0000000000..37a58dc71e Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/117.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/118.jpg b/packages/demo/resources/demo/assets/images/users/118.jpg new file mode 100644 index 0000000000..0f34d7e224 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/118.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/119.jpg b/packages/demo/resources/demo/assets/images/users/119.jpg new file mode 100644 index 0000000000..2237812457 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/119.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/12.jpg b/packages/demo/resources/demo/assets/images/users/12.jpg new file mode 100644 index 0000000000..873eb4036d Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/12.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/120.jpg b/packages/demo/resources/demo/assets/images/users/120.jpg new file mode 100644 index 0000000000..e823958024 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/120.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/121.jpg b/packages/demo/resources/demo/assets/images/users/121.jpg new file mode 100644 index 0000000000..77d2032115 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/121.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/122.jpg b/packages/demo/resources/demo/assets/images/users/122.jpg new file mode 100644 index 0000000000..bae017131d Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/122.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/123.jpg b/packages/demo/resources/demo/assets/images/users/123.jpg new file mode 100644 index 0000000000..0ca84caffe Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/123.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/124.jpg b/packages/demo/resources/demo/assets/images/users/124.jpg new file mode 100644 index 0000000000..8e36b968e5 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/124.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/125.jpg b/packages/demo/resources/demo/assets/images/users/125.jpg new file mode 100644 index 0000000000..e2665a7bbb Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/125.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/126.jpg b/packages/demo/resources/demo/assets/images/users/126.jpg new file mode 100644 index 0000000000..b1d3eece9e Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/126.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/127.jpg b/packages/demo/resources/demo/assets/images/users/127.jpg new file mode 100644 index 0000000000..53b6643fdb Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/127.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/128.jpg b/packages/demo/resources/demo/assets/images/users/128.jpg new file mode 100644 index 0000000000..6dcb0b0827 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/128.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/129.jpg b/packages/demo/resources/demo/assets/images/users/129.jpg new file mode 100644 index 0000000000..fb1a366e54 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/129.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/13.jpg b/packages/demo/resources/demo/assets/images/users/13.jpg new file mode 100644 index 0000000000..42a4bea6e7 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/13.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/130.jpg b/packages/demo/resources/demo/assets/images/users/130.jpg new file mode 100644 index 0000000000..b556ce666a Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/130.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/131.jpg b/packages/demo/resources/demo/assets/images/users/131.jpg new file mode 100644 index 0000000000..0c772be66c Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/131.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/132.jpg b/packages/demo/resources/demo/assets/images/users/132.jpg new file mode 100644 index 0000000000..2a518c348f Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/132.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/133.jpg b/packages/demo/resources/demo/assets/images/users/133.jpg new file mode 100644 index 0000000000..16c453adce Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/133.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/134.jpg b/packages/demo/resources/demo/assets/images/users/134.jpg new file mode 100644 index 0000000000..4cd0daf891 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/134.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/135.jpg b/packages/demo/resources/demo/assets/images/users/135.jpg new file mode 100644 index 0000000000..4bd9fee77b Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/135.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/136.jpg b/packages/demo/resources/demo/assets/images/users/136.jpg new file mode 100644 index 0000000000..86c1685489 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/136.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/137.jpg b/packages/demo/resources/demo/assets/images/users/137.jpg new file mode 100644 index 0000000000..d6fe0cbf21 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/137.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/138.jpg b/packages/demo/resources/demo/assets/images/users/138.jpg new file mode 100644 index 0000000000..c39440dda6 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/138.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/139.jpg b/packages/demo/resources/demo/assets/images/users/139.jpg new file mode 100644 index 0000000000..1fa269f54e Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/139.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/14.jpg b/packages/demo/resources/demo/assets/images/users/14.jpg new file mode 100644 index 0000000000..b08f892fdf Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/14.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/140.jpg b/packages/demo/resources/demo/assets/images/users/140.jpg new file mode 100644 index 0000000000..b1bc48a8fa Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/140.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/141.jpg b/packages/demo/resources/demo/assets/images/users/141.jpg new file mode 100644 index 0000000000..a421f1a18c Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/141.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/142.jpg b/packages/demo/resources/demo/assets/images/users/142.jpg new file mode 100644 index 0000000000..f0ee233d61 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/142.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/143.jpg b/packages/demo/resources/demo/assets/images/users/143.jpg new file mode 100644 index 0000000000..1113446824 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/143.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/144.jpg b/packages/demo/resources/demo/assets/images/users/144.jpg new file mode 100644 index 0000000000..5ff31c7443 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/144.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/145.jpg b/packages/demo/resources/demo/assets/images/users/145.jpg new file mode 100644 index 0000000000..c1467c99e8 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/145.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/146.jpg b/packages/demo/resources/demo/assets/images/users/146.jpg new file mode 100644 index 0000000000..f8b357b458 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/146.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/147.jpg b/packages/demo/resources/demo/assets/images/users/147.jpg new file mode 100644 index 0000000000..257b0cb132 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/147.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/148.jpg b/packages/demo/resources/demo/assets/images/users/148.jpg new file mode 100644 index 0000000000..7a801528a1 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/148.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/149.jpg b/packages/demo/resources/demo/assets/images/users/149.jpg new file mode 100644 index 0000000000..73892b2829 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/149.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/15.jpg b/packages/demo/resources/demo/assets/images/users/15.jpg new file mode 100644 index 0000000000..d16fe44d12 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/15.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/150.jpg b/packages/demo/resources/demo/assets/images/users/150.jpg new file mode 100644 index 0000000000..e2d2e012c5 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/150.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/151.jpg b/packages/demo/resources/demo/assets/images/users/151.jpg new file mode 100644 index 0000000000..8070d5267a Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/151.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/152.jpg b/packages/demo/resources/demo/assets/images/users/152.jpg new file mode 100644 index 0000000000..1b7de4bf6b Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/152.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/153.jpg b/packages/demo/resources/demo/assets/images/users/153.jpg new file mode 100644 index 0000000000..7a99ecd33c Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/153.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/154.jpg b/packages/demo/resources/demo/assets/images/users/154.jpg new file mode 100644 index 0000000000..bd026ca7ff Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/154.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/155.jpg b/packages/demo/resources/demo/assets/images/users/155.jpg new file mode 100644 index 0000000000..cda9a82833 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/155.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/156.jpg b/packages/demo/resources/demo/assets/images/users/156.jpg new file mode 100644 index 0000000000..86901e3a7f Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/156.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/157.jpg b/packages/demo/resources/demo/assets/images/users/157.jpg new file mode 100644 index 0000000000..733aed9aa9 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/157.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/158.jpg b/packages/demo/resources/demo/assets/images/users/158.jpg new file mode 100644 index 0000000000..095ce52137 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/158.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/159.jpg b/packages/demo/resources/demo/assets/images/users/159.jpg new file mode 100644 index 0000000000..c82972507b Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/159.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/16.jpg b/packages/demo/resources/demo/assets/images/users/16.jpg new file mode 100644 index 0000000000..c1ad538e13 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/16.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/160.jpg b/packages/demo/resources/demo/assets/images/users/160.jpg new file mode 100644 index 0000000000..8f13b70f0b Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/160.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/161.jpg b/packages/demo/resources/demo/assets/images/users/161.jpg new file mode 100644 index 0000000000..3e600aadaf Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/161.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/162.jpg b/packages/demo/resources/demo/assets/images/users/162.jpg new file mode 100644 index 0000000000..11c149a0ae Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/162.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/163.jpg b/packages/demo/resources/demo/assets/images/users/163.jpg new file mode 100644 index 0000000000..76d619fa6b Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/163.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/164.jpg b/packages/demo/resources/demo/assets/images/users/164.jpg new file mode 100644 index 0000000000..9206a8364a Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/164.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/165.jpg b/packages/demo/resources/demo/assets/images/users/165.jpg new file mode 100644 index 0000000000..4eed47465f Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/165.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/166.jpg b/packages/demo/resources/demo/assets/images/users/166.jpg new file mode 100644 index 0000000000..2806f8864b Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/166.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/167.jpg b/packages/demo/resources/demo/assets/images/users/167.jpg new file mode 100644 index 0000000000..b640d36b9e Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/167.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/168.jpg b/packages/demo/resources/demo/assets/images/users/168.jpg new file mode 100644 index 0000000000..437f62f140 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/168.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/169.jpg b/packages/demo/resources/demo/assets/images/users/169.jpg new file mode 100644 index 0000000000..50f24fcfc1 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/169.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/17.jpg b/packages/demo/resources/demo/assets/images/users/17.jpg new file mode 100644 index 0000000000..17b803f889 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/17.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/170.jpg b/packages/demo/resources/demo/assets/images/users/170.jpg new file mode 100644 index 0000000000..09e744ac33 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/170.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/171.jpg b/packages/demo/resources/demo/assets/images/users/171.jpg new file mode 100644 index 0000000000..fdbde699ec Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/171.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/172.jpg b/packages/demo/resources/demo/assets/images/users/172.jpg new file mode 100644 index 0000000000..70fdc8411f Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/172.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/173.jpg b/packages/demo/resources/demo/assets/images/users/173.jpg new file mode 100644 index 0000000000..de24548d82 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/173.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/174.jpg b/packages/demo/resources/demo/assets/images/users/174.jpg new file mode 100644 index 0000000000..b7d4d47909 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/174.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/175.jpg b/packages/demo/resources/demo/assets/images/users/175.jpg new file mode 100644 index 0000000000..f92989f851 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/175.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/176.jpg b/packages/demo/resources/demo/assets/images/users/176.jpg new file mode 100644 index 0000000000..18411e07b7 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/176.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/177.jpg b/packages/demo/resources/demo/assets/images/users/177.jpg new file mode 100644 index 0000000000..0f81037319 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/177.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/178.jpg b/packages/demo/resources/demo/assets/images/users/178.jpg new file mode 100644 index 0000000000..58fdda7e93 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/178.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/179.jpg b/packages/demo/resources/demo/assets/images/users/179.jpg new file mode 100644 index 0000000000..48e6b8130a Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/179.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/18.jpg b/packages/demo/resources/demo/assets/images/users/18.jpg new file mode 100644 index 0000000000..e74be54734 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/18.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/180.jpg b/packages/demo/resources/demo/assets/images/users/180.jpg new file mode 100644 index 0000000000..112d592576 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/180.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/181.jpg b/packages/demo/resources/demo/assets/images/users/181.jpg new file mode 100644 index 0000000000..61c8b0c4bd Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/181.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/182.jpg b/packages/demo/resources/demo/assets/images/users/182.jpg new file mode 100644 index 0000000000..882a762a02 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/182.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/183.jpg b/packages/demo/resources/demo/assets/images/users/183.jpg new file mode 100644 index 0000000000..5d76cfe508 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/183.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/184.jpg b/packages/demo/resources/demo/assets/images/users/184.jpg new file mode 100644 index 0000000000..fbf99e567f Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/184.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/185.jpg b/packages/demo/resources/demo/assets/images/users/185.jpg new file mode 100644 index 0000000000..ec56f2e3c7 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/185.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/186.jpg b/packages/demo/resources/demo/assets/images/users/186.jpg new file mode 100644 index 0000000000..ab6a30532a Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/186.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/187.jpg b/packages/demo/resources/demo/assets/images/users/187.jpg new file mode 100644 index 0000000000..a0e202b9cc Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/187.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/188.jpg b/packages/demo/resources/demo/assets/images/users/188.jpg new file mode 100644 index 0000000000..ce2e9cb548 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/188.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/189.jpg b/packages/demo/resources/demo/assets/images/users/189.jpg new file mode 100644 index 0000000000..f1ba4f33a4 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/189.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/19.jpg b/packages/demo/resources/demo/assets/images/users/19.jpg new file mode 100644 index 0000000000..be7099aee7 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/19.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/190.jpg b/packages/demo/resources/demo/assets/images/users/190.jpg new file mode 100644 index 0000000000..24335ea391 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/190.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/191.jpg b/packages/demo/resources/demo/assets/images/users/191.jpg new file mode 100644 index 0000000000..1d526a4292 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/191.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/192.jpg b/packages/demo/resources/demo/assets/images/users/192.jpg new file mode 100644 index 0000000000..9d79272c33 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/192.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/193.jpg b/packages/demo/resources/demo/assets/images/users/193.jpg new file mode 100644 index 0000000000..161d7e5870 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/193.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/194.jpg b/packages/demo/resources/demo/assets/images/users/194.jpg new file mode 100644 index 0000000000..cf171238ff Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/194.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/195.jpg b/packages/demo/resources/demo/assets/images/users/195.jpg new file mode 100644 index 0000000000..2e13ab15cd Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/195.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/196.jpg b/packages/demo/resources/demo/assets/images/users/196.jpg new file mode 100644 index 0000000000..f1142b5ec1 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/196.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/197.jpg b/packages/demo/resources/demo/assets/images/users/197.jpg new file mode 100644 index 0000000000..e89128094c Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/197.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/198.jpg b/packages/demo/resources/demo/assets/images/users/198.jpg new file mode 100644 index 0000000000..3d09dcc5b0 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/198.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/199.jpg b/packages/demo/resources/demo/assets/images/users/199.jpg new file mode 100644 index 0000000000..850c3e406f Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/199.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/2.jpg b/packages/demo/resources/demo/assets/images/users/2.jpg new file mode 100644 index 0000000000..5569d497f6 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/2.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/20.jpg b/packages/demo/resources/demo/assets/images/users/20.jpg new file mode 100644 index 0000000000..5f1b89e6d7 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/20.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/200.jpg b/packages/demo/resources/demo/assets/images/users/200.jpg new file mode 100644 index 0000000000..09fa1529fb Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/200.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/201.jpg b/packages/demo/resources/demo/assets/images/users/201.jpg new file mode 100644 index 0000000000..13ca3bfa1f Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/201.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/202.jpg b/packages/demo/resources/demo/assets/images/users/202.jpg new file mode 100644 index 0000000000..b5d0f56a06 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/202.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/203.jpg b/packages/demo/resources/demo/assets/images/users/203.jpg new file mode 100644 index 0000000000..7678da665f Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/203.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/204.jpg b/packages/demo/resources/demo/assets/images/users/204.jpg new file mode 100644 index 0000000000..a563383070 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/204.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/205.jpg b/packages/demo/resources/demo/assets/images/users/205.jpg new file mode 100644 index 0000000000..dae407cc4c Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/205.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/206.jpg b/packages/demo/resources/demo/assets/images/users/206.jpg new file mode 100644 index 0000000000..eb38bc4e37 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/206.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/207.jpg b/packages/demo/resources/demo/assets/images/users/207.jpg new file mode 100644 index 0000000000..a2b64ab577 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/207.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/208.jpg b/packages/demo/resources/demo/assets/images/users/208.jpg new file mode 100644 index 0000000000..7f6e8c5d7d Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/208.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/209.jpg b/packages/demo/resources/demo/assets/images/users/209.jpg new file mode 100644 index 0000000000..6308b7ffea Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/209.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/21.jpg b/packages/demo/resources/demo/assets/images/users/21.jpg new file mode 100644 index 0000000000..5b21c6cc37 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/21.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/210.jpg b/packages/demo/resources/demo/assets/images/users/210.jpg new file mode 100644 index 0000000000..cdcd7bbb29 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/210.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/211.jpg b/packages/demo/resources/demo/assets/images/users/211.jpg new file mode 100644 index 0000000000..122c4c2274 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/211.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/212.jpg b/packages/demo/resources/demo/assets/images/users/212.jpg new file mode 100644 index 0000000000..36a123cc6c Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/212.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/213.jpg b/packages/demo/resources/demo/assets/images/users/213.jpg new file mode 100644 index 0000000000..7bbc696d9f Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/213.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/214.jpg b/packages/demo/resources/demo/assets/images/users/214.jpg new file mode 100644 index 0000000000..4b0ce5f78a Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/214.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/215.jpg b/packages/demo/resources/demo/assets/images/users/215.jpg new file mode 100644 index 0000000000..85fe596390 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/215.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/216.jpg b/packages/demo/resources/demo/assets/images/users/216.jpg new file mode 100644 index 0000000000..7d5eff0f18 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/216.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/217.jpg b/packages/demo/resources/demo/assets/images/users/217.jpg new file mode 100644 index 0000000000..4353655233 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/217.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/218.jpg b/packages/demo/resources/demo/assets/images/users/218.jpg new file mode 100644 index 0000000000..5eceb0c4c7 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/218.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/219.jpg b/packages/demo/resources/demo/assets/images/users/219.jpg new file mode 100644 index 0000000000..4d49f1acd4 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/219.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/22.jpg b/packages/demo/resources/demo/assets/images/users/22.jpg new file mode 100644 index 0000000000..3635637ec8 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/22.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/220.jpg b/packages/demo/resources/demo/assets/images/users/220.jpg new file mode 100644 index 0000000000..b3489e46b1 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/220.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/221.jpg b/packages/demo/resources/demo/assets/images/users/221.jpg new file mode 100644 index 0000000000..6c5a31db3c Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/221.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/222.jpg b/packages/demo/resources/demo/assets/images/users/222.jpg new file mode 100644 index 0000000000..1f7e86d593 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/222.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/23.jpg b/packages/demo/resources/demo/assets/images/users/23.jpg new file mode 100644 index 0000000000..ec0d5f33c4 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/23.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/24.jpg b/packages/demo/resources/demo/assets/images/users/24.jpg new file mode 100644 index 0000000000..64d5340c93 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/24.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/25.jpg b/packages/demo/resources/demo/assets/images/users/25.jpg new file mode 100644 index 0000000000..b4b26f187f Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/25.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/26.jpg b/packages/demo/resources/demo/assets/images/users/26.jpg new file mode 100644 index 0000000000..95fa435e9f Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/26.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/27.jpg b/packages/demo/resources/demo/assets/images/users/27.jpg new file mode 100644 index 0000000000..2eca82b9de Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/27.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/28.jpg b/packages/demo/resources/demo/assets/images/users/28.jpg new file mode 100644 index 0000000000..58991878e8 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/28.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/29.jpg b/packages/demo/resources/demo/assets/images/users/29.jpg new file mode 100644 index 0000000000..54141261e3 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/29.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/3.jpg b/packages/demo/resources/demo/assets/images/users/3.jpg new file mode 100644 index 0000000000..e67555dea3 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/3.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/30.jpg b/packages/demo/resources/demo/assets/images/users/30.jpg new file mode 100644 index 0000000000..3625c93bff Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/30.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/31.jpg b/packages/demo/resources/demo/assets/images/users/31.jpg new file mode 100644 index 0000000000..5a76b10343 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/31.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/32.jpg b/packages/demo/resources/demo/assets/images/users/32.jpg new file mode 100644 index 0000000000..23ff991c44 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/32.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/33.jpg b/packages/demo/resources/demo/assets/images/users/33.jpg new file mode 100644 index 0000000000..349454eea4 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/33.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/34.jpg b/packages/demo/resources/demo/assets/images/users/34.jpg new file mode 100644 index 0000000000..2a066e68c9 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/34.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/35.jpg b/packages/demo/resources/demo/assets/images/users/35.jpg new file mode 100644 index 0000000000..0ac6a02681 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/35.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/36.jpg b/packages/demo/resources/demo/assets/images/users/36.jpg new file mode 100644 index 0000000000..171eb981ed Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/36.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/37.jpg b/packages/demo/resources/demo/assets/images/users/37.jpg new file mode 100644 index 0000000000..83b4f5b1e2 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/37.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/38.jpg b/packages/demo/resources/demo/assets/images/users/38.jpg new file mode 100644 index 0000000000..66175c8177 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/38.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/39.jpg b/packages/demo/resources/demo/assets/images/users/39.jpg new file mode 100644 index 0000000000..ae89d1032b Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/39.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/4.jpg b/packages/demo/resources/demo/assets/images/users/4.jpg new file mode 100644 index 0000000000..060af74d6c Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/4.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/40.jpg b/packages/demo/resources/demo/assets/images/users/40.jpg new file mode 100644 index 0000000000..9fde626963 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/40.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/41.jpg b/packages/demo/resources/demo/assets/images/users/41.jpg new file mode 100644 index 0000000000..d07733252a Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/41.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/42.jpg b/packages/demo/resources/demo/assets/images/users/42.jpg new file mode 100644 index 0000000000..93263d3e9c Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/42.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/43.jpg b/packages/demo/resources/demo/assets/images/users/43.jpg new file mode 100644 index 0000000000..b090630ff7 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/43.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/44.jpg b/packages/demo/resources/demo/assets/images/users/44.jpg new file mode 100644 index 0000000000..0547184ae7 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/44.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/45.jpg b/packages/demo/resources/demo/assets/images/users/45.jpg new file mode 100644 index 0000000000..23286669b6 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/45.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/46.jpg b/packages/demo/resources/demo/assets/images/users/46.jpg new file mode 100644 index 0000000000..c47dd3ffbf Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/46.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/47.jpg b/packages/demo/resources/demo/assets/images/users/47.jpg new file mode 100644 index 0000000000..6fa65684fd Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/47.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/48.jpg b/packages/demo/resources/demo/assets/images/users/48.jpg new file mode 100644 index 0000000000..4b733f829e Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/48.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/49.jpg b/packages/demo/resources/demo/assets/images/users/49.jpg new file mode 100644 index 0000000000..8f7bd123bc Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/49.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/5.jpg b/packages/demo/resources/demo/assets/images/users/5.jpg new file mode 100644 index 0000000000..11613432e2 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/5.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/50.jpg b/packages/demo/resources/demo/assets/images/users/50.jpg new file mode 100644 index 0000000000..38c1b2ea31 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/50.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/51.jpg b/packages/demo/resources/demo/assets/images/users/51.jpg new file mode 100644 index 0000000000..55bc1341cb Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/51.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/52.jpg b/packages/demo/resources/demo/assets/images/users/52.jpg new file mode 100644 index 0000000000..292498e77e Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/52.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/53.jpg b/packages/demo/resources/demo/assets/images/users/53.jpg new file mode 100644 index 0000000000..a5badb5fd7 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/53.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/54.jpg b/packages/demo/resources/demo/assets/images/users/54.jpg new file mode 100644 index 0000000000..b94d8f6c95 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/54.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/55.jpg b/packages/demo/resources/demo/assets/images/users/55.jpg new file mode 100644 index 0000000000..cfaff49ee5 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/55.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/56.jpg b/packages/demo/resources/demo/assets/images/users/56.jpg new file mode 100644 index 0000000000..3e150a2f84 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/56.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/57.jpg b/packages/demo/resources/demo/assets/images/users/57.jpg new file mode 100644 index 0000000000..de94f76cc1 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/57.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/58.jpg b/packages/demo/resources/demo/assets/images/users/58.jpg new file mode 100644 index 0000000000..a67f0d59bd Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/58.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/59.jpg b/packages/demo/resources/demo/assets/images/users/59.jpg new file mode 100644 index 0000000000..79784a59cb Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/59.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/6.jpg b/packages/demo/resources/demo/assets/images/users/6.jpg new file mode 100644 index 0000000000..e21ad1a4fe Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/6.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/60.jpg b/packages/demo/resources/demo/assets/images/users/60.jpg new file mode 100644 index 0000000000..09aabd5642 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/60.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/61.jpg b/packages/demo/resources/demo/assets/images/users/61.jpg new file mode 100644 index 0000000000..1236f27cc2 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/61.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/62.jpg b/packages/demo/resources/demo/assets/images/users/62.jpg new file mode 100644 index 0000000000..430ca55705 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/62.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/63.jpg b/packages/demo/resources/demo/assets/images/users/63.jpg new file mode 100644 index 0000000000..20fd0d2b4b Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/63.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/64.jpg b/packages/demo/resources/demo/assets/images/users/64.jpg new file mode 100644 index 0000000000..fc6621c155 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/64.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/65.jpg b/packages/demo/resources/demo/assets/images/users/65.jpg new file mode 100644 index 0000000000..7b54bbdc61 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/65.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/66.jpg b/packages/demo/resources/demo/assets/images/users/66.jpg new file mode 100644 index 0000000000..42ed29665e Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/66.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/67.jpg b/packages/demo/resources/demo/assets/images/users/67.jpg new file mode 100644 index 0000000000..1ff014f60c Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/67.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/68.jpg b/packages/demo/resources/demo/assets/images/users/68.jpg new file mode 100644 index 0000000000..0b54245332 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/68.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/69.jpg b/packages/demo/resources/demo/assets/images/users/69.jpg new file mode 100644 index 0000000000..40ece13496 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/69.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/7.jpg b/packages/demo/resources/demo/assets/images/users/7.jpg new file mode 100644 index 0000000000..45b21a5a6e Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/7.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/70.jpg b/packages/demo/resources/demo/assets/images/users/70.jpg new file mode 100644 index 0000000000..0d3c1cc6a9 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/70.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/71.jpg b/packages/demo/resources/demo/assets/images/users/71.jpg new file mode 100644 index 0000000000..b0d73bc31c Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/71.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/72.jpg b/packages/demo/resources/demo/assets/images/users/72.jpg new file mode 100644 index 0000000000..2bbe3da878 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/72.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/73.jpg b/packages/demo/resources/demo/assets/images/users/73.jpg new file mode 100644 index 0000000000..8c79fc9df1 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/73.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/74.jpg b/packages/demo/resources/demo/assets/images/users/74.jpg new file mode 100644 index 0000000000..86de97fe3b Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/74.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/75.jpg b/packages/demo/resources/demo/assets/images/users/75.jpg new file mode 100644 index 0000000000..e8a49c6469 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/75.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/76.jpg b/packages/demo/resources/demo/assets/images/users/76.jpg new file mode 100644 index 0000000000..49b75ec0c2 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/76.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/77.jpg b/packages/demo/resources/demo/assets/images/users/77.jpg new file mode 100644 index 0000000000..0fc03ca8f0 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/77.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/78.jpg b/packages/demo/resources/demo/assets/images/users/78.jpg new file mode 100644 index 0000000000..22a85a954b Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/78.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/79.jpg b/packages/demo/resources/demo/assets/images/users/79.jpg new file mode 100644 index 0000000000..efbf6e22c3 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/79.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/8.jpg b/packages/demo/resources/demo/assets/images/users/8.jpg new file mode 100644 index 0000000000..49be885588 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/8.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/80.jpg b/packages/demo/resources/demo/assets/images/users/80.jpg new file mode 100644 index 0000000000..91e1a0ab19 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/80.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/81.jpg b/packages/demo/resources/demo/assets/images/users/81.jpg new file mode 100644 index 0000000000..2a4f7a8f1c Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/81.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/82.jpg b/packages/demo/resources/demo/assets/images/users/82.jpg new file mode 100644 index 0000000000..d775d2bb5a Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/82.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/83.jpg b/packages/demo/resources/demo/assets/images/users/83.jpg new file mode 100644 index 0000000000..ff1a459824 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/83.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/84.jpg b/packages/demo/resources/demo/assets/images/users/84.jpg new file mode 100644 index 0000000000..b7530a1f73 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/84.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/85.jpg b/packages/demo/resources/demo/assets/images/users/85.jpg new file mode 100644 index 0000000000..4c3a7df70c Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/85.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/86.jpg b/packages/demo/resources/demo/assets/images/users/86.jpg new file mode 100644 index 0000000000..935ced91c6 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/86.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/87.jpg b/packages/demo/resources/demo/assets/images/users/87.jpg new file mode 100644 index 0000000000..d59ddc665d Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/87.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/88.jpg b/packages/demo/resources/demo/assets/images/users/88.jpg new file mode 100644 index 0000000000..65fec24209 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/88.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/89.jpg b/packages/demo/resources/demo/assets/images/users/89.jpg new file mode 100644 index 0000000000..f58ab3e544 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/89.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/9.jpg b/packages/demo/resources/demo/assets/images/users/9.jpg new file mode 100644 index 0000000000..589464ee98 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/9.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/90.jpg b/packages/demo/resources/demo/assets/images/users/90.jpg new file mode 100644 index 0000000000..92726a6cc5 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/90.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/91.jpg b/packages/demo/resources/demo/assets/images/users/91.jpg new file mode 100644 index 0000000000..f0ed5f2743 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/91.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/92.jpg b/packages/demo/resources/demo/assets/images/users/92.jpg new file mode 100644 index 0000000000..dfeb53bf26 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/92.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/93.jpg b/packages/demo/resources/demo/assets/images/users/93.jpg new file mode 100644 index 0000000000..a2b060cdc0 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/93.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/94.jpg b/packages/demo/resources/demo/assets/images/users/94.jpg new file mode 100644 index 0000000000..35a4b19773 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/94.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/95.jpg b/packages/demo/resources/demo/assets/images/users/95.jpg new file mode 100644 index 0000000000..11a95f73c4 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/95.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/96.jpg b/packages/demo/resources/demo/assets/images/users/96.jpg new file mode 100644 index 0000000000..768a36e0e7 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/96.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/97.jpg b/packages/demo/resources/demo/assets/images/users/97.jpg new file mode 100644 index 0000000000..e9d9639e9d Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/97.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/98.jpg b/packages/demo/resources/demo/assets/images/users/98.jpg new file mode 100644 index 0000000000..74a1d997a9 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/98.jpg differ diff --git a/packages/demo/resources/demo/assets/images/users/99.jpg b/packages/demo/resources/demo/assets/images/users/99.jpg new file mode 100644 index 0000000000..a541737437 Binary files /dev/null and b/packages/demo/resources/demo/assets/images/users/99.jpg differ diff --git a/packages/demo/resources/demo/assets/manifest.json b/packages/demo/resources/demo/assets/manifest.json new file mode 100644 index 0000000000..a89874a45f --- /dev/null +++ b/packages/demo/resources/demo/assets/manifest.json @@ -0,0 +1,1361 @@ +{ + "version": 1, + "generated": "2026-05-27", + "files": [ + { + "path": "files/audio/sample.mp3", + "bytes": 1954212, + "mime": "audio/mpeg" + }, + { + "path": "files/documents/sample.docx", + "bytes": 1311881, + "mime": "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + }, + { + "path": "files/documents/sample.txt", + "bytes": 607, + "mime": "text/plain" + }, + { + "path": "files/documents/sample.xlsx", + "bytes": 29380, + "mime": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + }, + { + "path": "files/pdf/sample-1.pdf", + "bytes": 581407, + "mime": "application/pdf" + }, + { + "path": "files/pdf/sample-2.pdf", + "bytes": 65715, + "mime": "application/pdf" + }, + { + "path": "files/pdf/sample-3.pdf", + "bytes": 1253607, + "mime": "application/pdf" + }, + { + "path": "files/pdf/sample-dummy.pdf", + "bytes": 13264, + "mime": "application/pdf" + }, + { + "path": "images/products/product-001.jpg", + "bytes": 50128, + "mime": "image/jpeg" + }, + { + "path": "images/products/product-002.jpg", + "bytes": 57911, + "mime": "image/jpeg" + }, + { + "path": "images/products/product-003.jpg", + "bytes": 47217, + "mime": "image/jpeg" + }, + { + "path": "images/products/product-004.jpg", + "bytes": 45884, + "mime": "image/jpeg" + }, + { + "path": "images/products/product-005.jpg", + "bytes": 52866, + "mime": "image/jpeg" + }, + { + "path": "images/products/product-006.jpg", + "bytes": 43634, + "mime": "image/jpeg" + }, + { + "path": "images/products/product-007.jpg", + "bytes": 60531, + "mime": "image/jpeg" + }, + { + "path": "images/products/product-008.jpg", + "bytes": 72482, + "mime": "image/jpeg" + }, + { + "path": "images/products/product-009.jpg", + "bytes": 73453, + "mime": "image/jpeg" + }, + { + "path": "images/products/product-010.jpg", + "bytes": 95203, + "mime": "image/jpeg" + }, + { + "path": "images/products/product-011.jpg", + "bytes": 64037, + "mime": "image/jpeg" + }, + { + "path": "images/products/product-012.jpg", + "bytes": 79079, + "mime": "image/jpeg" + }, + { + "path": "images/products/product-013.jpg", + "bytes": 138668, + "mime": "image/jpeg" + }, + { + "path": "images/products/product-014.jpg", + "bytes": 102107, + "mime": "image/jpeg" + }, + { + "path": "images/products/product-015.jpg", + "bytes": 143624, + "mime": "image/jpeg" + }, + { + "path": "images/products/product-016.jpg", + "bytes": 87333, + "mime": "image/jpeg" + }, + { + "path": "images/products/product-017.jpg", + "bytes": 164019, + "mime": "image/jpeg" + }, + { + "path": "images/products/product-018.jpg", + "bytes": 80836, + "mime": "image/jpeg" + }, + { + "path": "images/products/product-019.jpg", + "bytes": 116291, + "mime": "image/jpeg" + }, + { + "path": "images/products/product-020.jpg", + "bytes": 72907, + "mime": "image/jpeg" + }, + { + "path": "images/products/product-021.jpg", + "bytes": 112587, + "mime": "image/jpeg" + }, + { + "path": "images/products/product-022.jpg", + "bytes": 118602, + "mime": "image/jpeg" + }, + { + "path": "images/products/product-023.jpg", + "bytes": 32258, + "mime": "image/jpeg" + }, + { + "path": "images/products/product-024.jpg", + "bytes": 34396, + "mime": "image/jpeg" + }, + { + "path": "images/products/product-025.jpg", + "bytes": 139665, + "mime": "image/jpeg" + }, + { + "path": "images/products/product-026.jpg", + "bytes": 74744, + "mime": "image/jpeg" + }, + { + "path": "images/products/product-027.jpg", + "bytes": 90191, + "mime": "image/jpeg" + }, + { + "path": "images/products/product-028.jpg", + "bytes": 170197, + "mime": "image/jpeg" + }, + { + "path": "images/products/product-029.jpg", + "bytes": 177426, + "mime": "image/jpeg" + }, + { + "path": "images/products/product-030.jpg", + "bytes": 58352, + "mime": "image/jpeg" + }, + { + "path": "images/products/product-031.jpg", + "bytes": 28618, + "mime": "image/jpeg" + }, + { + "path": "images/products/product-032.jpg", + "bytes": 130518, + "mime": "image/jpeg" + }, + { + "path": "images/products/product-033.jpg", + "bytes": 62735, + "mime": "image/jpeg" + }, + { + "path": "images/products/product-034.jpg", + "bytes": 171782, + "mime": "image/jpeg" + }, + { + "path": "images/products/product-035.jpg", + "bytes": 54768, + "mime": "image/jpeg" + }, + { + "path": "images/products/product-036.webp", + "bytes": 42668, + "mime": "image/webp" + }, + { + "path": "images/users/1.jpg", + "bytes": 288471, + "mime": "image/jpeg" + }, + { + "path": "images/users/10.jpg", + "bytes": 171077, + "mime": "image/jpeg" + }, + { + "path": "images/users/100.jpg", + "bytes": 62941, + "mime": "image/jpeg" + }, + { + "path": "images/users/101.jpg", + "bytes": 134041, + "mime": "image/jpeg" + }, + { + "path": "images/users/102.jpg", + "bytes": 76472, + "mime": "image/jpeg" + }, + { + "path": "images/users/103.jpg", + "bytes": 113533, + "mime": "image/jpeg" + }, + { + "path": "images/users/104.jpg", + "bytes": 103230, + "mime": "image/jpeg" + }, + { + "path": "images/users/105.jpg", + "bytes": 153973, + "mime": "image/jpeg" + }, + { + "path": "images/users/106.jpg", + "bytes": 87139, + "mime": "image/jpeg" + }, + { + "path": "images/users/107.jpg", + "bytes": 140238, + "mime": "image/jpeg" + }, + { + "path": "images/users/108.jpg", + "bytes": 77453, + "mime": "image/jpeg" + }, + { + "path": "images/users/109.jpg", + "bytes": 205315, + "mime": "image/jpeg" + }, + { + "path": "images/users/11.jpg", + "bytes": 220295, + "mime": "image/jpeg" + }, + { + "path": "images/users/110.jpg", + "bytes": 68503, + "mime": "image/jpeg" + }, + { + "path": "images/users/111.jpg", + "bytes": 90351, + "mime": "image/jpeg" + }, + { + "path": "images/users/112.jpg", + "bytes": 91255, + "mime": "image/jpeg" + }, + { + "path": "images/users/113.jpg", + "bytes": 214157, + "mime": "image/jpeg" + }, + { + "path": "images/users/114.jpg", + "bytes": 99098, + "mime": "image/jpeg" + }, + { + "path": "images/users/115.jpg", + "bytes": 126137, + "mime": "image/jpeg" + }, + { + "path": "images/users/116.jpg", + "bytes": 139880, + "mime": "image/jpeg" + }, + { + "path": "images/users/117.jpg", + "bytes": 105379, + "mime": "image/jpeg" + }, + { + "path": "images/users/118.jpg", + "bytes": 116853, + "mime": "image/jpeg" + }, + { + "path": "images/users/119.jpg", + "bytes": 80182, + "mime": "image/jpeg" + }, + { + "path": "images/users/12.jpg", + "bytes": 174426, + "mime": "image/jpeg" + }, + { + "path": "images/users/120.jpg", + "bytes": 82935, + "mime": "image/jpeg" + }, + { + "path": "images/users/121.jpg", + "bytes": 113070, + "mime": "image/jpeg" + }, + { + "path": "images/users/122.jpg", + "bytes": 200069, + "mime": "image/jpeg" + }, + { + "path": "images/users/123.jpg", + "bytes": 124385, + "mime": "image/jpeg" + }, + { + "path": "images/users/124.jpg", + "bytes": 123985, + "mime": "image/jpeg" + }, + { + "path": "images/users/125.jpg", + "bytes": 136989, + "mime": "image/jpeg" + }, + { + "path": "images/users/126.jpg", + "bytes": 137593, + "mime": "image/jpeg" + }, + { + "path": "images/users/127.jpg", + "bytes": 174923, + "mime": "image/jpeg" + }, + { + "path": "images/users/128.jpg", + "bytes": 150242, + "mime": "image/jpeg" + }, + { + "path": "images/users/129.jpg", + "bytes": 159280, + "mime": "image/jpeg" + }, + { + "path": "images/users/13.jpg", + "bytes": 341315, + "mime": "image/jpeg" + }, + { + "path": "images/users/130.jpg", + "bytes": 71542, + "mime": "image/jpeg" + }, + { + "path": "images/users/131.jpg", + "bytes": 94918, + "mime": "image/jpeg" + }, + { + "path": "images/users/132.jpg", + "bytes": 108948, + "mime": "image/jpeg" + }, + { + "path": "images/users/133.jpg", + "bytes": 117484, + "mime": "image/jpeg" + }, + { + "path": "images/users/134.jpg", + "bytes": 93633, + "mime": "image/jpeg" + }, + { + "path": "images/users/135.jpg", + "bytes": 85252, + "mime": "image/jpeg" + }, + { + "path": "images/users/136.jpg", + "bytes": 119186, + "mime": "image/jpeg" + }, + { + "path": "images/users/137.jpg", + "bytes": 105688, + "mime": "image/jpeg" + }, + { + "path": "images/users/138.jpg", + "bytes": 92631, + "mime": "image/jpeg" + }, + { + "path": "images/users/139.jpg", + "bytes": 72617, + "mime": "image/jpeg" + }, + { + "path": "images/users/14.jpg", + "bytes": 390895, + "mime": "image/jpeg" + }, + { + "path": "images/users/140.jpg", + "bytes": 84801, + "mime": "image/jpeg" + }, + { + "path": "images/users/141.jpg", + "bytes": 79958, + "mime": "image/jpeg" + }, + { + "path": "images/users/142.jpg", + "bytes": 107344, + "mime": "image/jpeg" + }, + { + "path": "images/users/143.jpg", + "bytes": 84860, + "mime": "image/jpeg" + }, + { + "path": "images/users/144.jpg", + "bytes": 60045, + "mime": "image/jpeg" + }, + { + "path": "images/users/145.jpg", + "bytes": 121198, + "mime": "image/jpeg" + }, + { + "path": "images/users/146.jpg", + "bytes": 88573, + "mime": "image/jpeg" + }, + { + "path": "images/users/147.jpg", + "bytes": 106123, + "mime": "image/jpeg" + }, + { + "path": "images/users/148.jpg", + "bytes": 112449, + "mime": "image/jpeg" + }, + { + "path": "images/users/149.jpg", + "bytes": 80488, + "mime": "image/jpeg" + }, + { + "path": "images/users/15.jpg", + "bytes": 153449, + "mime": "image/jpeg" + }, + { + "path": "images/users/150.jpg", + "bytes": 84448, + "mime": "image/jpeg" + }, + { + "path": "images/users/151.jpg", + "bytes": 138031, + "mime": "image/jpeg" + }, + { + "path": "images/users/152.jpg", + "bytes": 104476, + "mime": "image/jpeg" + }, + { + "path": "images/users/153.jpg", + "bytes": 134248, + "mime": "image/jpeg" + }, + { + "path": "images/users/154.jpg", + "bytes": 234192, + "mime": "image/jpeg" + }, + { + "path": "images/users/155.jpg", + "bytes": 102424, + "mime": "image/jpeg" + }, + { + "path": "images/users/156.jpg", + "bytes": 97292, + "mime": "image/jpeg" + }, + { + "path": "images/users/157.jpg", + "bytes": 91618, + "mime": "image/jpeg" + }, + { + "path": "images/users/158.jpg", + "bytes": 102195, + "mime": "image/jpeg" + }, + { + "path": "images/users/159.jpg", + "bytes": 89056, + "mime": "image/jpeg" + }, + { + "path": "images/users/16.jpg", + "bytes": 54565, + "mime": "image/jpeg" + }, + { + "path": "images/users/160.jpg", + "bytes": 110639, + "mime": "image/jpeg" + }, + { + "path": "images/users/161.jpg", + "bytes": 95764, + "mime": "image/jpeg" + }, + { + "path": "images/users/162.jpg", + "bytes": 96154, + "mime": "image/jpeg" + }, + { + "path": "images/users/163.jpg", + "bytes": 99333, + "mime": "image/jpeg" + }, + { + "path": "images/users/164.jpg", + "bytes": 86868, + "mime": "image/jpeg" + }, + { + "path": "images/users/165.jpg", + "bytes": 90274, + "mime": "image/jpeg" + }, + { + "path": "images/users/166.jpg", + "bytes": 92728, + "mime": "image/jpeg" + }, + { + "path": "images/users/167.jpg", + "bytes": 126397, + "mime": "image/jpeg" + }, + { + "path": "images/users/168.jpg", + "bytes": 124292, + "mime": "image/jpeg" + }, + { + "path": "images/users/169.jpg", + "bytes": 105161, + "mime": "image/jpeg" + }, + { + "path": "images/users/17.jpg", + "bytes": 53718, + "mime": "image/jpeg" + }, + { + "path": "images/users/170.jpg", + "bytes": 85132, + "mime": "image/jpeg" + }, + { + "path": "images/users/171.jpg", + "bytes": 97844, + "mime": "image/jpeg" + }, + { + "path": "images/users/172.jpg", + "bytes": 93727, + "mime": "image/jpeg" + }, + { + "path": "images/users/173.jpg", + "bytes": 133072, + "mime": "image/jpeg" + }, + { + "path": "images/users/174.jpg", + "bytes": 92821, + "mime": "image/jpeg" + }, + { + "path": "images/users/175.jpg", + "bytes": 82329, + "mime": "image/jpeg" + }, + { + "path": "images/users/176.jpg", + "bytes": 104742, + "mime": "image/jpeg" + }, + { + "path": "images/users/177.jpg", + "bytes": 107860, + "mime": "image/jpeg" + }, + { + "path": "images/users/178.jpg", + "bytes": 69393, + "mime": "image/jpeg" + }, + { + "path": "images/users/179.jpg", + "bytes": 134141, + "mime": "image/jpeg" + }, + { + "path": "images/users/18.jpg", + "bytes": 148475, + "mime": "image/jpeg" + }, + { + "path": "images/users/180.jpg", + "bytes": 98376, + "mime": "image/jpeg" + }, + { + "path": "images/users/181.jpg", + "bytes": 171480, + "mime": "image/jpeg" + }, + { + "path": "images/users/182.jpg", + "bytes": 96975, + "mime": "image/jpeg" + }, + { + "path": "images/users/183.jpg", + "bytes": 109782, + "mime": "image/jpeg" + }, + { + "path": "images/users/184.jpg", + "bytes": 91378, + "mime": "image/jpeg" + }, + { + "path": "images/users/185.jpg", + "bytes": 103254, + "mime": "image/jpeg" + }, + { + "path": "images/users/186.jpg", + "bytes": 95761, + "mime": "image/jpeg" + }, + { + "path": "images/users/187.jpg", + "bytes": 82626, + "mime": "image/jpeg" + }, + { + "path": "images/users/188.jpg", + "bytes": 98412, + "mime": "image/jpeg" + }, + { + "path": "images/users/189.jpg", + "bytes": 98760, + "mime": "image/jpeg" + }, + { + "path": "images/users/19.jpg", + "bytes": 129730, + "mime": "image/jpeg" + }, + { + "path": "images/users/190.jpg", + "bytes": 100025, + "mime": "image/jpeg" + }, + { + "path": "images/users/191.jpg", + "bytes": 86735, + "mime": "image/jpeg" + }, + { + "path": "images/users/192.jpg", + "bytes": 88818, + "mime": "image/jpeg" + }, + { + "path": "images/users/193.jpg", + "bytes": 118547, + "mime": "image/jpeg" + }, + { + "path": "images/users/194.jpg", + "bytes": 117400, + "mime": "image/jpeg" + }, + { + "path": "images/users/195.jpg", + "bytes": 92419, + "mime": "image/jpeg" + }, + { + "path": "images/users/196.jpg", + "bytes": 90593, + "mime": "image/jpeg" + }, + { + "path": "images/users/197.jpg", + "bytes": 81800, + "mime": "image/jpeg" + }, + { + "path": "images/users/198.jpg", + "bytes": 107016, + "mime": "image/jpeg" + }, + { + "path": "images/users/199.jpg", + "bytes": 115919, + "mime": "image/jpeg" + }, + { + "path": "images/users/2.jpg", + "bytes": 103004, + "mime": "image/jpeg" + }, + { + "path": "images/users/20.jpg", + "bytes": 129423, + "mime": "image/jpeg" + }, + { + "path": "images/users/200.jpg", + "bytes": 80858, + "mime": "image/jpeg" + }, + { + "path": "images/users/201.jpg", + "bytes": 85913, + "mime": "image/jpeg" + }, + { + "path": "images/users/202.jpg", + "bytes": 112634, + "mime": "image/jpeg" + }, + { + "path": "images/users/203.jpg", + "bytes": 93656, + "mime": "image/jpeg" + }, + { + "path": "images/users/204.jpg", + "bytes": 107824, + "mime": "image/jpeg" + }, + { + "path": "images/users/205.jpg", + "bytes": 94274, + "mime": "image/jpeg" + }, + { + "path": "images/users/206.jpg", + "bytes": 114017, + "mime": "image/jpeg" + }, + { + "path": "images/users/207.jpg", + "bytes": 107153, + "mime": "image/jpeg" + }, + { + "path": "images/users/208.jpg", + "bytes": 91640, + "mime": "image/jpeg" + }, + { + "path": "images/users/209.jpg", + "bytes": 71990, + "mime": "image/jpeg" + }, + { + "path": "images/users/21.jpg", + "bytes": 62133, + "mime": "image/jpeg" + }, + { + "path": "images/users/210.jpg", + "bytes": 89030, + "mime": "image/jpeg" + }, + { + "path": "images/users/211.jpg", + "bytes": 104639, + "mime": "image/jpeg" + }, + { + "path": "images/users/212.jpg", + "bytes": 99795, + "mime": "image/jpeg" + }, + { + "path": "images/users/213.jpg", + "bytes": 78761, + "mime": "image/jpeg" + }, + { + "path": "images/users/214.jpg", + "bytes": 95607, + "mime": "image/jpeg" + }, + { + "path": "images/users/215.jpg", + "bytes": 117180, + "mime": "image/jpeg" + }, + { + "path": "images/users/216.jpg", + "bytes": 79368, + "mime": "image/jpeg" + }, + { + "path": "images/users/217.jpg", + "bytes": 76865, + "mime": "image/jpeg" + }, + { + "path": "images/users/218.jpg", + "bytes": 117072, + "mime": "image/jpeg" + }, + { + "path": "images/users/219.jpg", + "bytes": 82684, + "mime": "image/jpeg" + }, + { + "path": "images/users/22.jpg", + "bytes": 58725, + "mime": "image/jpeg" + }, + { + "path": "images/users/220.jpg", + "bytes": 111102, + "mime": "image/jpeg" + }, + { + "path": "images/users/221.jpg", + "bytes": 113405, + "mime": "image/jpeg" + }, + { + "path": "images/users/222.jpg", + "bytes": 117073, + "mime": "image/jpeg" + }, + { + "path": "images/users/23.jpg", + "bytes": 218177, + "mime": "image/jpeg" + }, + { + "path": "images/users/24.jpg", + "bytes": 80805, + "mime": "image/jpeg" + }, + { + "path": "images/users/25.jpg", + "bytes": 74635, + "mime": "image/jpeg" + }, + { + "path": "images/users/26.jpg", + "bytes": 78954, + "mime": "image/jpeg" + }, + { + "path": "images/users/27.jpg", + "bytes": 91722, + "mime": "image/jpeg" + }, + { + "path": "images/users/28.jpg", + "bytes": 57007, + "mime": "image/jpeg" + }, + { + "path": "images/users/29.jpg", + "bytes": 138777, + "mime": "image/jpeg" + }, + { + "path": "images/users/3.jpg", + "bytes": 124004, + "mime": "image/jpeg" + }, + { + "path": "images/users/30.jpg", + "bytes": 160445, + "mime": "image/jpeg" + }, + { + "path": "images/users/31.jpg", + "bytes": 123919, + "mime": "image/jpeg" + }, + { + "path": "images/users/32.jpg", + "bytes": 341768, + "mime": "image/jpeg" + }, + { + "path": "images/users/33.jpg", + "bytes": 227306, + "mime": "image/jpeg" + }, + { + "path": "images/users/34.jpg", + "bytes": 200505, + "mime": "image/jpeg" + }, + { + "path": "images/users/35.jpg", + "bytes": 82081, + "mime": "image/jpeg" + }, + { + "path": "images/users/36.jpg", + "bytes": 110265, + "mime": "image/jpeg" + }, + { + "path": "images/users/37.jpg", + "bytes": 169780, + "mime": "image/jpeg" + }, + { + "path": "images/users/38.jpg", + "bytes": 132920, + "mime": "image/jpeg" + }, + { + "path": "images/users/39.jpg", + "bytes": 109377, + "mime": "image/jpeg" + }, + { + "path": "images/users/4.jpg", + "bytes": 166310, + "mime": "image/jpeg" + }, + { + "path": "images/users/40.jpg", + "bytes": 155905, + "mime": "image/jpeg" + }, + { + "path": "images/users/41.jpg", + "bytes": 67400, + "mime": "image/jpeg" + }, + { + "path": "images/users/42.jpg", + "bytes": 53438, + "mime": "image/jpeg" + }, + { + "path": "images/users/43.jpg", + "bytes": 55666, + "mime": "image/jpeg" + }, + { + "path": "images/users/44.jpg", + "bytes": 96120, + "mime": "image/jpeg" + }, + { + "path": "images/users/45.jpg", + "bytes": 128623, + "mime": "image/jpeg" + }, + { + "path": "images/users/46.jpg", + "bytes": 57671, + "mime": "image/jpeg" + }, + { + "path": "images/users/47.jpg", + "bytes": 134383, + "mime": "image/jpeg" + }, + { + "path": "images/users/48.jpg", + "bytes": 68679, + "mime": "image/jpeg" + }, + { + "path": "images/users/49.jpg", + "bytes": 81896, + "mime": "image/jpeg" + }, + { + "path": "images/users/5.jpg", + "bytes": 116001, + "mime": "image/jpeg" + }, + { + "path": "images/users/50.jpg", + "bytes": 297253, + "mime": "image/jpeg" + }, + { + "path": "images/users/51.jpg", + "bytes": 253926, + "mime": "image/jpeg" + }, + { + "path": "images/users/52.jpg", + "bytes": 110024, + "mime": "image/jpeg" + }, + { + "path": "images/users/53.jpg", + "bytes": 51566, + "mime": "image/jpeg" + }, + { + "path": "images/users/54.jpg", + "bytes": 111380, + "mime": "image/jpeg" + }, + { + "path": "images/users/55.jpg", + "bytes": 194678, + "mime": "image/jpeg" + }, + { + "path": "images/users/56.jpg", + "bytes": 204713, + "mime": "image/jpeg" + }, + { + "path": "images/users/57.jpg", + "bytes": 65579, + "mime": "image/jpeg" + }, + { + "path": "images/users/58.jpg", + "bytes": 373924, + "mime": "image/jpeg" + }, + { + "path": "images/users/59.jpg", + "bytes": 383180, + "mime": "image/jpeg" + }, + { + "path": "images/users/6.jpg", + "bytes": 131406, + "mime": "image/jpeg" + }, + { + "path": "images/users/60.jpg", + "bytes": 128590, + "mime": "image/jpeg" + }, + { + "path": "images/users/61.jpg", + "bytes": 59602, + "mime": "image/jpeg" + }, + { + "path": "images/users/62.jpg", + "bytes": 151650, + "mime": "image/jpeg" + }, + { + "path": "images/users/63.jpg", + "bytes": 235497, + "mime": "image/jpeg" + }, + { + "path": "images/users/64.jpg", + "bytes": 173422, + "mime": "image/jpeg" + }, + { + "path": "images/users/65.jpg", + "bytes": 63466, + "mime": "image/jpeg" + }, + { + "path": "images/users/66.jpg", + "bytes": 190150, + "mime": "image/jpeg" + }, + { + "path": "images/users/67.jpg", + "bytes": 518835, + "mime": "image/jpeg" + }, + { + "path": "images/users/68.jpg", + "bytes": 86972, + "mime": "image/jpeg" + }, + { + "path": "images/users/69.jpg", + "bytes": 249235, + "mime": "image/jpeg" + }, + { + "path": "images/users/7.jpg", + "bytes": 148024, + "mime": "image/jpeg" + }, + { + "path": "images/users/70.jpg", + "bytes": 116818, + "mime": "image/jpeg" + }, + { + "path": "images/users/71.jpg", + "bytes": 173239, + "mime": "image/jpeg" + }, + { + "path": "images/users/72.jpg", + "bytes": 179854, + "mime": "image/jpeg" + }, + { + "path": "images/users/73.jpg", + "bytes": 74263, + "mime": "image/jpeg" + }, + { + "path": "images/users/74.jpg", + "bytes": 186418, + "mime": "image/jpeg" + }, + { + "path": "images/users/75.jpg", + "bytes": 72758, + "mime": "image/jpeg" + }, + { + "path": "images/users/76.jpg", + "bytes": 217855, + "mime": "image/jpeg" + }, + { + "path": "images/users/77.jpg", + "bytes": 139147, + "mime": "image/jpeg" + }, + { + "path": "images/users/78.jpg", + "bytes": 59655, + "mime": "image/jpeg" + }, + { + "path": "images/users/79.jpg", + "bytes": 66133, + "mime": "image/jpeg" + }, + { + "path": "images/users/8.jpg", + "bytes": 225208, + "mime": "image/jpeg" + }, + { + "path": "images/users/80.jpg", + "bytes": 108964, + "mime": "image/jpeg" + }, + { + "path": "images/users/81.jpg", + "bytes": 224962, + "mime": "image/jpeg" + }, + { + "path": "images/users/82.jpg", + "bytes": 133266, + "mime": "image/jpeg" + }, + { + "path": "images/users/83.jpg", + "bytes": 163804, + "mime": "image/jpeg" + }, + { + "path": "images/users/84.jpg", + "bytes": 232357, + "mime": "image/jpeg" + }, + { + "path": "images/users/85.jpg", + "bytes": 148290, + "mime": "image/jpeg" + }, + { + "path": "images/users/86.jpg", + "bytes": 246147, + "mime": "image/jpeg" + }, + { + "path": "images/users/87.jpg", + "bytes": 121447, + "mime": "image/jpeg" + }, + { + "path": "images/users/88.jpg", + "bytes": 156902, + "mime": "image/jpeg" + }, + { + "path": "images/users/89.jpg", + "bytes": 184290, + "mime": "image/jpeg" + }, + { + "path": "images/users/9.jpg", + "bytes": 63930, + "mime": "image/jpeg" + }, + { + "path": "images/users/90.jpg", + "bytes": 143987, + "mime": "image/jpeg" + }, + { + "path": "images/users/91.jpg", + "bytes": 71432, + "mime": "image/jpeg" + }, + { + "path": "images/users/92.jpg", + "bytes": 84143, + "mime": "image/jpeg" + }, + { + "path": "images/users/93.jpg", + "bytes": 78712, + "mime": "image/jpeg" + }, + { + "path": "images/users/94.jpg", + "bytes": 111875, + "mime": "image/jpeg" + }, + { + "path": "images/users/95.jpg", + "bytes": 106348, + "mime": "image/jpeg" + }, + { + "path": "images/users/96.jpg", + "bytes": 129348, + "mime": "image/jpeg" + }, + { + "path": "images/users/97.jpg", + "bytes": 105024, + "mime": "image/jpeg" + }, + { + "path": "images/users/98.jpg", + "bytes": 130337, + "mime": "image/jpeg" + }, + { + "path": "images/users/99.jpg", + "bytes": 61866, + "mime": "image/jpeg" + }, + { + "path": "videos/short/big-buck-bunny-360.mp4", + "bytes": 991017, + "mime": "video/mp4" + }, + { + "path": "videos/short/flower.webm", + "bytes": 554058, + "mime": "video/webm" + }, + { + "path": "videos/short/sample-10s.mp4", + "bytes": 5485935, + "mime": "video/mp4" + }, + { + "path": "videos/short/sample-5s.mp4", + "bytes": 2848208, + "mime": "video/mp4" + }, + { + "path": "videos/short/sample-mp4.mp4", + "bytes": 10546620, + "mime": "video/mp4" + } + ] +} diff --git a/packages/demo/resources/demo/assets/videos/short/big-buck-bunny-360.mp4 b/packages/demo/resources/demo/assets/videos/short/big-buck-bunny-360.mp4 new file mode 100644 index 0000000000..9b6d89da00 Binary files /dev/null and b/packages/demo/resources/demo/assets/videos/short/big-buck-bunny-360.mp4 differ diff --git a/packages/demo/resources/demo/assets/videos/short/flower.webm b/packages/demo/resources/demo/assets/videos/short/flower.webm new file mode 100644 index 0000000000..5b8edf7d83 Binary files /dev/null and b/packages/demo/resources/demo/assets/videos/short/flower.webm differ diff --git a/packages/demo/resources/demo/assets/videos/short/sample-10s.mp4 b/packages/demo/resources/demo/assets/videos/short/sample-10s.mp4 new file mode 100644 index 0000000000..e0be482dfc Binary files /dev/null and b/packages/demo/resources/demo/assets/videos/short/sample-10s.mp4 differ diff --git a/packages/demo/resources/demo/assets/videos/short/sample-5s.mp4 b/packages/demo/resources/demo/assets/videos/short/sample-5s.mp4 new file mode 100644 index 0000000000..7936dc0909 Binary files /dev/null and b/packages/demo/resources/demo/assets/videos/short/sample-5s.mp4 differ diff --git a/packages/demo/resources/demo/assets/videos/short/sample-mp4.mp4 b/packages/demo/resources/demo/assets/videos/short/sample-mp4.mp4 new file mode 100644 index 0000000000..24b738c4f7 Binary files /dev/null and b/packages/demo/resources/demo/assets/videos/short/sample-mp4.mp4 differ diff --git a/packages/demo/src/Console/Commands/BatchJobCommand.php b/packages/demo/src/Commands/BatchJobCommand.php similarity index 88% rename from packages/demo/src/Console/Commands/BatchJobCommand.php rename to packages/demo/src/Commands/BatchJobCommand.php index e2e03c490a..73c41675e7 100644 --- a/packages/demo/src/Console/Commands/BatchJobCommand.php +++ b/packages/demo/src/Commands/BatchJobCommand.php @@ -1,9 +1,9 @@ option('dataset'); + $sizes = config('demo.dataset_sizes', []); + $datasetCount = $sizes[$dataset] ?? $sizes[config('demo.default_dataset', 'small')] ?? 100; + + $locales = $this->resolveLocales(); + + $context = new DemoContext( + languageCount: (int) $this->option('languages'), + locales: $locales, + dataset: $dataset, + datasetCount: $datasetCount, + fresh: (bool) $this->option('fresh'), + skipSeeders: (bool) $this->option('skip-seeders'), + skipFactories: (bool) $this->option('skip-factories'), + skipMedia: (bool) $this->option('skip-media'), + ); + + return (new DemoRunner($this))->run($context); + } + + /** + * @return list + */ + private function resolveLocales(): array + { + $localesOption = $this->option('locales'); + + if (is_string($localesOption) && $localesOption !== '') { + return array_values(array_filter(array_map( + static fn (string $value): string => trim($value), + explode(',', $localesOption) + ))); + } + + $defaults = config('demo.default_locales', ['cs_CZ', 'en_US', 'de_DE', 'pl_PL']); + $count = max(1, (int) $this->option('languages')); + + return array_slice($defaults, 0, $count); + } +} diff --git a/packages/demo/src/Console/Commands/DemoJobCommand.php b/packages/demo/src/Commands/DemoJobCommand.php similarity index 88% rename from packages/demo/src/Console/Commands/DemoJobCommand.php rename to packages/demo/src/Commands/DemoJobCommand.php index 16822126ff..a7473a75b3 100644 --- a/packages/demo/src/Console/Commands/DemoJobCommand.php +++ b/packages/demo/src/Commands/DemoJobCommand.php @@ -1,9 +1,9 @@ stopIndicator(); + + $this->command->newLine(); + $this->command->line("▶ {$title}"); + } + + public function startTask(string $label): void + { + $this->stopIndicator(); + + $this->indicatorLabel = $label; + $this->indicator = new ProgressIndicator( + $this->command->getOutput(), + '🌱 %message%' + ); + $this->indicator->start($label); + } + + /** + * Stop any spinner and print a running label — use before nested seeder output. + */ + public function beginNestedOutput(string $label): void + { + $this->stopIndicator(); + $this->command->line(" … {$label}"); + } + + public function updateTask(string $label): void + { + $this->indicator?->setMessage($label); + } + + public function finishTask(string $label, ?string $detail = null): void + { + if ($this->indicator === null) { + $this->command->line(" ✓ {$label}".($detail !== null ? " ({$detail})" : '')); + + return; + } + + $this->indicator->finish($detail ?? $label, '✓'); + $this->indicator = null; + $this->indicatorLabel = null; + } + + public function failTask(string $label, string $error): void + { + if ($this->indicator !== null) { + $this->indicator->finish($label, '✗'); + $this->indicator = null; + $this->indicatorLabel = null; + } + + $this->command->error(" ✗ {$label}: {$error}"); + } + + public function skip(string $label, ?string $reason = null): void + { + $this->stopIndicator(); + + $suffix = $reason !== null ? " — {$reason}" : ''; + + $this->command->line(" ○ {$label}{$suffix}"); + } + + public function detail(string $line): void + { + $this->command->line(" │ {$line}"); + } + + public function created(string $label): void + { + $this->command->line(" + {$label}"); + } + + public function progressBar(int $max, string $message): DemoProgressBar + { + $this->stopIndicator(); + + $bar = new ProgressBar($this->command->getOutput(), max(1, $max)); + $bar->setFormat(' │ %current%/%max% [%bar%] %message%'); + $bar->setMessage($message); + $bar->start(); + + return new DemoProgressBar($bar, $this->command, $message); + } + + private function stopIndicator(): void + { + if ($this->indicator === null) { + return; + } + + $this->indicator->finish($this->indicatorLabel ?? ''); + $this->indicator = null; + $this->indicatorLabel = null; + } +} diff --git a/packages/demo/src/Console/DemoProgressBar.php b/packages/demo/src/Console/DemoProgressBar.php new file mode 100644 index 0000000000..7c3551dc94 --- /dev/null +++ b/packages/demo/src/Console/DemoProgressBar.php @@ -0,0 +1,35 @@ +bar->advance($step); + } + + public function setMessage(string $message): void + { + $this->bar->setMessage($message); + } + + public function finish(?string $summary = null): void + { + $this->bar->setMessage($summary ?? $this->defaultMessage); + + $this->bar->finish(); + $this->command->newLine(); + } +} diff --git a/packages/demo/src/Demo/DemoContext.php b/packages/demo/src/Demo/DemoContext.php new file mode 100644 index 0000000000..720025d254 --- /dev/null +++ b/packages/demo/src/Demo/DemoContext.php @@ -0,0 +1,22 @@ + $locales + */ + public function __construct( + public readonly int $languageCount, + public readonly array $locales, + public readonly string $dataset, + public readonly int $datasetCount, + public readonly bool $fresh, + public readonly bool $skipSeeders, + public readonly bool $skipFactories, + public readonly bool $skipMedia, + ) {} +} diff --git a/packages/demo/src/Demo/DemoRunner.php b/packages/demo/src/Demo/DemoRunner.php new file mode 100644 index 0000000000..52528e8fc7 --- /dev/null +++ b/packages/demo/src/Demo/DemoRunner.php @@ -0,0 +1,100 @@ +resolver = new SeederOrderResolver; + $this->console = new DemoConsole($this->command); + } + + public function run(DemoContext $context): int + { + $this->applyDemoContext($context); + SeedOutput::bind($this->console); + + $this->command->info('Moox Demo — seeding installed Moox packages...'); + $this->console->detail("Dataset: {$context->dataset} ({$context->datasetCount} records per factory entity)"); + + $this->console->phase('Database'); + (new FreshDatabaseStep($this->command, $this->console))->run($context); + + $seeders = new PackageSeedersStep($this->command, $this->resolver, $this->console); + + $this->console->phase('Static data'); + $seeders->run($context, onlySlugs: ['data']); + + $this->console->phase('Localizations'); + (new DemoLocalizationStep($this->console))->run($context); + + $this->console->phase('Media'); + (new DemoMediaStep($this->command, $this->console))->run($context); + + $this->console->phase('Users'); + $seeders->run($context, onlySlugs: ['user']); + (new DemoUserStep($this->command, $this->console))->run($context); + + $this->console->phase('Package seeders'); + $seeders->run($context, exceptSlugs: ['data', 'demo', 'user']); + + $this->console->phase('Factory entities'); + (new FactoryEntitiesStep($this->console))->run($context); + + SeedOutput::bind(null); + + $this->command->newLine(); + $this->command->info('Moox Demo finished.'); + + return Command::SUCCESS; + } + + private function applyDemoContext(DemoContext $context): void + { + config([ + 'demo.runtime.seeding' => true, + 'demo.runtime.skip_media' => $context->skipMedia, + 'demo.dataset' => $context->dataset, + 'demo.dataset_count' => $context->datasetCount, + 'demo.media.users_path' => $this->resolveDemoUsersMediaPath(), + ]); + } + + private function resolveDemoUsersMediaPath(): ?string + { + $configured = config('demo.media.users_path'); + + if (is_string($configured) && $configured !== '' && is_dir($configured)) { + return $configured; + } + + $composerPath = MooxPackageDiscovery::composerPathForPackage('moox/demo'); + + if ($composerPath === null) { + return null; + } + + $path = dirname($composerPath).'/resources/demo/assets/images/users'; + + return is_dir($path) ? $path : null; + } +} diff --git a/packages/demo/src/Demo/SeederOrderResolver.php b/packages/demo/src/Demo/SeederOrderResolver.php new file mode 100644 index 0000000000..92e6fca7da --- /dev/null +++ b/packages/demo/src/Demo/SeederOrderResolver.php @@ -0,0 +1,161 @@ + + */ + public function resolve(): array + { + $providers = MooxPackageDiscovery::scanMooxProviders(); + $skipSlugs = config('demo.seeder_skip', []); + $nestedBasenames = config('demo.nested_seeder_basenames', []); + $priority = array_flip(config('demo.seeder_order', [])); + + $candidates = []; + + foreach ($providers as $packageName => $providerClass) { + $slug = MooxPackageDiscovery::packageSlug($packageName); + + if (in_array($slug, $skipSlugs, true)) { + continue; + } + + $entry = $this->resolveEntrySeeder($packageName, $providerClass, $nestedBasenames); + + if ($entry === null) { + continue; + } + + $candidates[$slug] = [ + 'package' => $packageName, + 'slug' => $slug, + 'class' => $entry['class'], + 'seeder' => $entry['name'], + ]; + } + + $sortedSlugs = $this->topologicalSort(array_keys($candidates), $priority); + + $ordered = []; + foreach ($sortedSlugs as $slug) { + if (isset($candidates[$slug])) { + $ordered[] = $candidates[$slug]; + } + } + + return $ordered; + } + + /** + * @param list $nestedBasenames + * @return array{name: string, class: string|null}|null + */ + private function resolveEntrySeeder(string $packageName, string $providerClass, array $nestedBasenames): ?array + { + $composer = MooxPackageDiscovery::readComposerJson($packageName); + $seedPath = $composer['extra']['moox']['install']['seed'] ?? null; + + if (is_string($seedPath) && $seedPath !== '') { + $basename = basename($seedPath, '.php'); + $class = MooxPackageDiscovery::resolveSeederClass($packageName, $basename); + + return [ + 'name' => $basename, + 'class' => $class, + ]; + } + + try { + $instance = new $providerClass(app()); + $mooxInfo = $instance->mooxInfo(); + $seeders = $mooxInfo['seeders'] ?? []; + } catch (\Throwable) { + return null; + } + + foreach ($seeders as $seederName) { + if (in_array($seederName, $nestedBasenames, true)) { + continue; + } + + if (str_ends_with($seederName, 'DatabaseSeeder')) { + continue; + } + + $class = MooxPackageDiscovery::resolveSeederClass($packageName, $seederName); + + return [ + 'name' => $seederName, + 'class' => $class, + ]; + } + + return null; + } + + /** + * @param list $slugs + * @param array $priority + * @return list + */ + private function topologicalSort(array $slugs, array $priority): array + { + $graph = MooxPackageDiscovery::mooxDependencyGraph(); + $slugSet = array_flip($slugs); + $inDegree = array_fill_keys($slugs, 0); + $adjacency = array_fill_keys($slugs, []); + + foreach ($slugs as $slug) { + foreach ($graph[$slug] ?? [] as $dep) { + if (! isset($slugSet[$dep])) { + continue; + } + + $adjacency[$dep][] = $slug; + $inDegree[$slug]++; + } + } + + $queue = []; + foreach ($slugs as $slug) { + if ($inDegree[$slug] === 0) { + $queue[] = $slug; + } + } + + usort($queue, fn (string $a, string $b): int => ($priority[$a] ?? 999) <=> ($priority[$b] ?? 999)); + + $sorted = []; + + while ($queue !== []) { + $current = array_shift($queue); + $sorted[] = $current; + + foreach ($adjacency[$current] ?? [] as $neighbor) { + $inDegree[$neighbor]--; + if ($inDegree[$neighbor] === 0) { + $queue[] = $neighbor; + } + } + + usort($queue, fn (string $a, string $b): int => ($priority[$a] ?? 999) <=> ($priority[$b] ?? 999)); + } + + if (count($sorted) !== count($slugs)) { + foreach ($slugs as $slug) { + if (! in_array($slug, $sorted, true)) { + $sorted[] = $slug; + } + } + } + + return $sorted; + } +} diff --git a/packages/demo/src/Demo/Steps/DemoLocalizationStep.php b/packages/demo/src/Demo/Steps/DemoLocalizationStep.php new file mode 100644 index 0000000000..bc6d97cbcd --- /dev/null +++ b/packages/demo/src/Demo/Steps/DemoLocalizationStep.php @@ -0,0 +1,90 @@ +console->skip('Localizations', 'moox/localization not installed'); + + return; + } + + if (! $this->tableExists('static_languages')) { + $this->console->failTask('Localizations', 'static_languages table missing — run moox/data first'); + + return; + } + + $locales = MooxSeederLocales::mergeForDemoRun($context->locales); + + if ($locales === []) { + $this->console->skip('Localizations', 'no locales configured'); + + return; + } + + $languages = DB::table('static_languages')->pluck('id', 'alpha2'); + + if ($languages->isEmpty()) { + $this->console->failTask('Localizations', 'no rows in static_languages — seed moox/data first'); + + return; + } + + $modelClass = Localization::class; + + $this->console->beginNestedOutput('Localizations'); + + foreach ($locales as $index => $localeVariant) { + $alpha2 = strtolower(substr($localeVariant, 0, 2)); + $languageId = $languages->get($alpha2) ?? $languages->first(); + + $slug = strtolower(str_replace('_', '-', $localeVariant)); + $title = str_replace('_', ' ', $localeVariant); + + $modelClass::query()->updateOrCreate( + ['locale_variant' => $localeVariant], + [ + 'title' => $title, + 'slug' => $slug, + 'language_id' => $languageId, + 'fallback_language_id' => null, + 'is_active_admin' => true, + 'is_active_frontend' => true, + 'is_default' => $index === 0, + 'fallback_behaviour' => 'default', + 'language_routing' => 'path', + 'routing_path' => $slug, + 'routing_subdomain' => null, + 'routing_domain' => null, + 'translation_status' => 100, + 'language_settings' => json_encode(['locale' => $localeVariant]), + ] + ); + + $this->console->created("Localization {$localeVariant}"); + } + + $this->console->finishTask('Localizations', count($locales).' locale(s)'); + } + + private function tableExists(string $table): bool + { + return DB::getSchemaBuilder()->hasTable($table); + } +} diff --git a/packages/demo/src/Demo/Steps/DemoMediaStep.php b/packages/demo/src/Demo/Steps/DemoMediaStep.php new file mode 100644 index 0000000000..91db909bf6 --- /dev/null +++ b/packages/demo/src/Demo/Steps/DemoMediaStep.php @@ -0,0 +1,64 @@ +skipMedia) { + $this->console->skip('Demo media', 'skipped via --skip-media'); + + return; + } + + $sourceDir = dirname(__DIR__, 3).'/resources/demo/media'; + + if (! is_dir($sourceDir)) { + if ($this->command->getOutput()->isVerbose()) { + $this->console->skip('Demo media', 'no media directory found'); + } + + return; + } + + $disk = (string) config('demo.media.disk', 'public'); + $directory = (string) config('demo.media.directory', 'demo'); + + $files = File::files($sourceDir); + + if ($files === []) { + $this->console->skip('Demo media', 'no files to copy'); + + return; + } + + $this->console->beginNestedOutput('Demo media'); + + foreach ($files as $file) { + $relative = $directory.'/'.$file->getFilename(); + Storage::disk($disk)->put($relative, File::get($file->getPathname())); + $this->console->created($file->getFilename()); + } + + $this->console->finishTask('Demo media', count($files).' file(s) copied'); + + if (class_exists(Media::class)) { + $this->console->detail('moox/media is installed; entity seeders attach media via Mediathek.'); + } + } +} diff --git a/packages/demo/src/Demo/Steps/DemoUserStep.php b/packages/demo/src/Demo/Steps/DemoUserStep.php new file mode 100644 index 0000000000..3ad82929c2 --- /dev/null +++ b/packages/demo/src/Demo/Steps/DemoUserStep.php @@ -0,0 +1,69 @@ +command->getOutput()->isVerbose()) { + $this->console->detail('Demo user step skipped — UserSeeder handles demo users.'); + } + + return; + } + + $config = config('demo.demo_user', []); + + if (! ($config['enabled'] ?? true)) { + $this->console->skip('Demo user', 'disabled in config'); + + return; + } + + $userClass = config('auth.providers.users.model', 'App\\Models\\User'); + + if (! class_exists($userClass)) { + $this->console->skip('Demo user', 'user model not found'); + + return; + } + + if ($userClass::query()->exists()) { + if ($this->command->getOutput()->isVerbose()) { + $this->console->skip('Demo user', 'users already exist'); + } + + return; + } + + $email = (string) ($config['email'] ?? 'demo@moox.org'); + $name = (string) ($config['name'] ?? 'Moox Demo'); + + $this->console->beginNestedOutput('Demo user'); + + $userClass::query()->create([ + 'name' => $name, + 'email' => $email, + 'password' => Hash::make((string) ($config['password'] ?? 'password')), + 'email_verified_at' => now(), + ]); + + $this->console->created("User {$email}"); + $this->console->finishTask('Demo user'); + } +} diff --git a/packages/demo/src/Demo/Steps/FactoryEntitiesStep.php b/packages/demo/src/Demo/Steps/FactoryEntitiesStep.php new file mode 100644 index 0000000000..f9d1b9f2b6 --- /dev/null +++ b/packages/demo/src/Demo/Steps/FactoryEntitiesStep.php @@ -0,0 +1,135 @@ +skipFactories) { + $this->console->skip('Factory seeding', 'skipped via --skip-factories'); + + return; + } + + config([ + 'demo.locales' => $context->locales, + 'demo.dataset_count' => $context->datasetCount, + ]); + + foreach (MooxPackageDiscovery::mooxPackageNames() as $packageName) { + $composer = MooxPackageDiscovery::readComposerJson($packageName); + + if ($composer === null) { + continue; + } + + $entities = $composer['extra']['moox']['install']['auto_entities'] ?? []; + $classes = $composer['extra']['moox']['install']['auto_class'] ?? []; + + foreach ($entities as $entityName => $enabled) { + if (! $enabled) { + continue; + } + + $modelClass = $classes[$entityName] ?? null; + + if (! is_string($modelClass) || ! class_exists($modelClass)) { + continue; + } + + if (! is_subclass_of($modelClass, Model::class)) { + continue; + } + + if (! $this->modelUsesFactory($modelClass)) { + continue; + } + + $factory = $this->resolveFactory($modelClass); + + if ($factory === null) { + continue; + } + + $label = class_basename($modelClass); + + try { + $created = $this->seedWithFactory($factory, $context, $label); + $this->console->finishTask("{$packageName} · {$label}", "{$created} record(s)"); + } catch (\Throwable $e) { + $this->console->failTask("{$packageName} · {$label}", $e->getMessage()); + } + } + } + } + + private function modelUsesFactory(string $modelClass): bool + { + return in_array( + HasFactory::class, + class_uses_recursive($modelClass), + true + ); + } + + private function resolveFactory(string $modelClass): ?Factory + { + if (! method_exists($modelClass, 'factory')) { + return null; + } + + return $modelClass::factory(); + } + + private function seedWithFactory(Factory $factory, DemoContext $context, string $label): int + { + $count = $context->datasetCount; + $locales = $context->locales; + $progress = $this->console->progressBar($count, $label); + + if ($locales !== [] && method_exists($factory, 'withLocales')) { + for ($i = 0; $i < $count; $i++) { + $factory->withLocales($locales)->create(); + $progress->advance(); + } + + $progress->finish("{$count} record(s)"); + + return $count; + } + + if ($locales !== [] && method_exists($factory, 'withTranslationLocales')) { + for ($i = 0; $i < $count; $i++) { + $factory->withTranslationLocales(...$locales)->create(); + $progress->advance(); + } + + $progress->finish("{$count} record(s)"); + + return $count; + } + + for ($i = 0; $i < $count; $i++) { + $factory->create(); + $progress->advance(); + } + + $progress->finish("{$count} record(s)"); + + return $count; + } +} diff --git a/packages/demo/src/Demo/Steps/FreshDatabaseStep.php b/packages/demo/src/Demo/Steps/FreshDatabaseStep.php new file mode 100644 index 0000000000..2fa3a11338 --- /dev/null +++ b/packages/demo/src/Demo/Steps/FreshDatabaseStep.php @@ -0,0 +1,38 @@ +fresh) { + $this->console->skip('migrate:fresh', 'not requested'); + + return; + } + + if (! $this->command->option('no-interaction')) { + if (! $this->command->confirm('This will run migrate:fresh and erase all data. Continue?', false)) { + $this->console->skip('migrate:fresh', 'cancelled'); + + return; + } + } + + $this->console->beginNestedOutput('migrate:fresh'); + $this->command->call('migrate:fresh', ['--force' => true]); + $this->console->finishTask('migrate:fresh'); + } +} diff --git a/packages/demo/src/Demo/Steps/PackageSeedersStep.php b/packages/demo/src/Demo/Steps/PackageSeedersStep.php new file mode 100644 index 0000000000..5ad64267a9 --- /dev/null +++ b/packages/demo/src/Demo/Steps/PackageSeedersStep.php @@ -0,0 +1,133 @@ +|null $onlySlugs + * @param list $exceptSlugs + */ + public function run(DemoContext $context, ?array $onlySlugs = null, array $exceptSlugs = []): void + { + if ($context->skipSeeders) { + $this->console->skip('Package seeders', 'skipped via --skip-seeders'); + + return; + } + + $failedSlugs = []; + $ordered = $this->resolver->resolve(); + + foreach ($ordered as $item) { + $slug = $item['slug']; + + if ($onlySlugs !== null && ! in_array($slug, $onlySlugs, true)) { + continue; + } + + if (in_array($slug, $exceptSlugs, true)) { + continue; + } + + foreach ($failedSlugs as $failed) { + if ($this->dependsOn($slug, $failed)) { + $this->console->skip( + $item['package'], + "dependency {$failed} failed" + ); + + continue 2; + } + } + + $class = $item['class']; + + if ($class === null || ! class_exists($class)) { + $this->console->skip( + $item['package'], + "seeder not found ({$item['seeder']})" + ); + + continue; + } + + if ($this->shouldSkipSlugForLocalization($slug, $context)) { + if ($this->command->getOutput()->isVerbose()) { + $this->console->skip($item['package'], 'using Demo localization step'); + } + + continue; + } + + $label = "{$item['package']} · {$item['seeder']}"; + + try { + $this->console->beginNestedOutput($label); + $this->invokeSeeder($class); + $this->console->finishTask($label); + } catch (\Throwable $e) { + $failedSlugs[] = $slug; + $this->console->failTask($label, $e->getMessage()); + } + } + } + + private function invokeSeeder(string $class): void + { + /** @var Seeder $seeder */ + $seeder = app()->make($class); + $seeder->setContainer(app()); + $seeder->setCommand($this->command); + $seeder->__invoke(); + } + + private function shouldSkipSlugForLocalization(string $slug, DemoContext $context): bool + { + return $slug === 'localization' + && class_exists(Localization::class) + && $context->locales !== []; + } + + private function dependsOn(string $slug, string $failedSlug): bool + { + $graph = MooxPackageDiscovery::mooxDependencyGraph(); + $visited = []; + $stack = [$slug]; + + while ($stack !== []) { + $current = array_pop($stack); + if ($current === $failedSlug) { + return true; + } + + if (isset($visited[$current])) { + continue; + } + + $visited[$current] = true; + + foreach ($graph[$current] ?? [] as $dep) { + $stack[] = $dep; + } + } + + return false; + } +} diff --git a/packages/demo/src/DemoServiceProvider.php b/packages/demo/src/DemoServiceProvider.php new file mode 100644 index 0000000000..23a58d39b4 --- /dev/null +++ b/packages/demo/src/DemoServiceProvider.php @@ -0,0 +1,31 @@ +name('demo') + ->hasConfigFile() + ->hasCommands( + DemoCommand::class, + ); + + $this->getMooxPackage() + ->title('Moox Demo') + ->released(false) + ->stability('dev') + ->category('development') + ->usedFor([ + 'seeding demo data for installed Moox packages', + ]); + } +} diff --git a/packages/demo/src/Jobs/BatchJob.php b/packages/demo/src/Jobs/BatchJob.php index 10e2ccdd33..dcfdf5d22f 100644 --- a/packages/demo/src/Jobs/BatchJob.php +++ b/packages/demo/src/Jobs/BatchJob.php @@ -1,6 +1,6 @@ */ + private const LOCALES_REQUIRING_REAL_TEXT = ['cs_CZ', 'en_US', 'de_DE', 'pl_PL']; + + /** @var array */ + private const TEXT_PRESET_CHARS = [ + 'title' => [13, 35], + 'tag_title' => [8, 23], + 'subtitle' => [20, 50], + 'excerpt' => [40, 90], + 'description' => [40, 70], + 'body' => [35, 80], + 'content' => [55, 120], + ]; + + protected function formatFakerWords(string $locale, Generator $faker, int $minWords, int $maxWords): string + { + $preset = $maxWords <= 3 ? 'tag_title' : 'title'; + + return $this->fakerLocaleTitle($locale, $faker, $preset); + } + + protected function formatFakerSentence(string $locale, Generator $faker, int $minWords = 2, int $maxWords = 4): string + { + unset($minWords, $maxWords); + + return $this->fakerLocaleTitle($locale, $faker, 'title'); + } + + protected function formatFakerPlainText(string $locale, string $text): string + { + if ($locale === 'en_US') { + return Str::title($text); + } + + return $text; + } + + /** + * Markdown body with heading + paragraphs — locale-sprachig via realText (Locale-Lock). + */ + protected function markdownContentFromLocale( + string $locale, + Generator $localeFaker, + int $minParagraphs = 3, + int $maxParagraphs = 6, + ?int $minChars = null, + ?int $maxChars = null, + ): string { + $heading = $this->fakerLocaleTitle($locale, $localeFaker, 'title'); + $paragraphMin = $minChars ?? self::TEXT_PRESET_CHARS['body'][0]; + $paragraphMax = $maxChars ?? self::TEXT_PRESET_CHARS['body'][1]; + $paragraphs = $this->fakerLocaleParagraphs( + $locale, + $localeFaker, + $minParagraphs, + $maxParagraphs, + $paragraphMin, + $paragraphMax, + ); + + return '## '.$heading."\n\n".implode("\n\n", $paragraphs); + } + + protected function localeSupportsRealText(Generator $faker): bool + { + try { + $sample = $faker->realTextBetween(20, 40); + + return trim($sample) !== ''; + } catch (\Throwable) { + return false; + } + } + + /** + * @param 'title'|'tag_title' $preset + */ + protected function fakerLocaleTitle( + string $locale, + Generator $faker, + string $preset = 'title', + ): string { + [$minChars, $maxChars] = self::TEXT_PRESET_CHARS[$preset]; + + return $this->fakerLocaleSentence($locale, $faker, $minChars, $maxChars); + } + + /** + * @param 'title'|'tag_title'|'subtitle'|'excerpt'|'description'|'body'|'content'|null $preset + */ + protected function fakerLocaleText( + string $locale, + Generator $faker, + ?int $minChars = null, + ?int $maxChars = null, + ?string $preset = null, + ?int $limit = null, + ): string { + if ($preset !== null) { + [$presetMin, $presetMax] = self::TEXT_PRESET_CHARS[$preset]; + $minChars ??= $presetMin; + $maxChars ??= $presetMax; + } + + [$minChars, $maxChars] = $this->normalizeCharRange($minChars ?? 35, $maxChars ?? 80); + + $text = trim($this->generateLocaleFließtext($locale, $faker, $minChars, $maxChars)); + + if ($limit !== null) { + $text = Str::limit($text, $limit, ''); + } + + return $this->formatFakerPlainText($locale, $text); + } + + protected function fakerLocaleSentence( + string $locale, + Generator $faker, + int $minChars = 13, + int $maxChars = 35, + ): string { + $chunk = $this->generateLocaleFließtext($locale, $faker, $minChars, $maxChars); + $sentence = $this->extractFirstSentence($chunk); + + return $this->formatFakerPlainText($locale, $sentence); + } + + /** + * @return list + */ + protected function fakerLocaleParagraphs( + string $locale, + Generator $faker, + int $minParagraphs = 3, + int $maxParagraphs = 6, + int $minChars = 35, + int $maxChars = 80, + ): array { + $count = random_int(min($minParagraphs, $maxParagraphs), max($minParagraphs, $maxParagraphs)); + $paragraphs = []; + + for ($i = 0; $i < $count; $i++) { + $paragraphs[] = $this->fakerLocaleText($locale, $faker, $minChars, $maxChars); + } + + return $paragraphs; + } + + protected function generateLocaleFließtext( + string $locale, + Generator $faker, + int $minChars, + int $maxChars, + ): string { + $this->assertLocaleUsesRealText($locale, $faker); + + return $faker->realTextBetween($minChars, $maxChars); + } + + protected function assertLocaleUsesRealText(string $locale, Generator $faker): void + { + if (! in_array($locale, self::LOCALES_REQUIRING_REAL_TEXT, true)) { + return; + } + + if ($this->localeSupportsRealText($faker)) { + return; + } + + throw new RuntimeException( + "Faker realTextBetween is required for locale [{$locale}] but is not available. ". + 'Use Faker\\Factory::create(\''.$locale.'\') via fakerForLocale().' + ); + } + + protected function extractFirstSentence(string $text): string + { + $text = trim($text); + + if ($text === '') { + return ''; + } + + $parts = preg_split('/(?<=[.!?…])\s+/u', $text, 2); + + return trim($parts[0] ?? $text); + } + + /** + * @return array{0: int, 1: int} + */ + protected function normalizeCharRange(int $minChars, int $maxChars): array + { + if ($minChars > $maxChars) { + return [$maxChars, $minChars]; + } + + return [$minChars, $maxChars]; + } +} diff --git a/packages/demo/src/Seeding/ImportDemoMediaToMediathek.php b/packages/demo/src/Seeding/ImportDemoMediaToMediathek.php new file mode 100644 index 0000000000..3934fb0d0c --- /dev/null +++ b/packages/demo/src/Seeding/ImportDemoMediaToMediathek.php @@ -0,0 +1,231 @@ + + */ + public static function listImagePaths(?string $sourceDir, int $limit): array + { + if ($sourceDir === null || ! is_dir($sourceDir) || $limit < 1) { + return []; + } + + $files = []; + foreach (['jpg', 'jpeg', 'png', 'webp'] as $extension) { + $matched = File::glob($sourceDir.'/*.'.$extension) ?: []; + foreach ($matched as $path) { + if (is_string($path) && is_file($path)) { + $files[] = $path; + } + } + } + $files = array_values(array_unique($files)); + sort($files, SORT_NATURAL); + + return array_slice($files, 0, $limit); + } + + /** + * Import a single demo file into the mediathek (skips upload if file_hash already exists). + */ + public static function importFromPath(string $path, ?int $mediaCollectionId = null): ?Media + { + if (! class_exists(Media::class) || ! is_file($path)) { + return null; + } + + $collection = self::resolveMediaCollection($mediaCollectionId); + + if ($collection === null) { + return null; + } + + return self::importFile( + $path, + $collection, + self::resolveCollectionName($collection), + (string) config('app.locale', 'en_US'), + ); + } + + /** + * @return array{id: int, file_name: string, title: string, description: null, internal_note: null, alt: string} + */ + public static function avatarPayloadFromMedia(Media $media): array + { + $title = self::resolveTitle($media); + + return [ + 'id' => (int) $media->getKey(), + 'file_name' => (string) $media->file_name, + 'title' => $title, + 'description' => null, + 'internal_note' => null, + 'alt' => $title, + ]; + } + + public static function avatarUrlFromMedia(Media $media): string + { + return json_encode(self::avatarPayloadFromMedia($media), JSON_UNESCAPED_UNICODE); + } + + private static function importFile( + string $path, + MediaCollection $collection, + string $collectionName, + string $locale, + ): ?Media { + $originalName = basename($path); + $fileHash = hash_file('sha256', $path); + + if ($fileHash === false) { + return null; + } + + $existingMedia = Media::query() + ->where('custom_properties->file_hash', $fileHash) + ->first(); + + if ($existingMedia instanceof Media) { + return $existingMedia; + } + + $mimeType = mime_content_type($path) ?: 'image/jpeg'; + + $uploadedFile = new UploadedFile( + $path, + $originalName, + $mimeType, + null, + true, + ); + + $model = new Media; + $model->exists = true; + + /** @var Media $media */ + $media = app(FileAdderFactory::class) + ->create($model, $uploadedFile) + ->preservingOriginal() + ->toMediaCollection($collectionName); + + $media->media_collection_id = $collection->getKey(); + $media->collection_name = $collectionName; + $media->original_model_type = Media::class; + $media->original_model_id = $media->getKey(); + $media->model_id = $media->getKey(); + $media->model_type = Media::class; + $media->setCustomProperty('file_hash', $fileHash); + + if (str_starts_with($media->mime_type, 'image/')) { + try { + $mediaPath = $media->getPath(); + if ($mediaPath !== '') { + $size = @getimagesize($mediaPath); + if (is_array($size)) { + $media->setCustomProperty('dimensions', [ + 'width' => (int) $size[0], + 'height' => (int) $size[1], + ]); + } + } + } catch (\Throwable) { + // ignore + } + } + + $media->save(); + + $titleFallback = pathinfo($originalName, PATHINFO_FILENAME); + + $translation = $media->translateOrNew($locale); + $translation->setAttribute('name', $originalName); + $translation->setAttribute('title', $titleFallback); + $translation->setAttribute('alt', $titleFallback); + $translation->save(); + + $media->setAttribute('title', $titleFallback); + $media->setAttribute('alt', $titleFallback); + + return $media; + } + + private static function resolveMediaCollection(?int $mediaCollectionId): ?MediaCollection + { + MediaCollection::ensureUncategorizedExists(); + + if ($mediaCollectionId !== null) { + $collection = MediaCollection::query()->find($mediaCollectionId); + + if ($collection instanceof MediaCollection) { + return $collection; + } + } + + return MediaCollection::query()->with('translations')->orderBy('id')->first(); + } + + private static function resolveCollectionName(MediaCollection $collection): string + { + $locale = (string) config('app.locale', 'en_US'); + $fallback = (string) config('app.fallback_locale', 'en_US'); + + foreach ([$locale, $fallback, 'en_US'] as $candidate) { + $translation = $collection->translate($candidate, false); + $name = self::translationName($translation); + if ($name !== null) { + return $name; + } + } + + if ($collection->relationLoaded('translations') && $collection->translations->isNotEmpty()) { + $name = self::translationName($collection->translations->first()); + if ($name !== null) { + return $name; + } + } + + return (string) $collection->getKey(); + } + + private static function translationName(?Model $translation): ?string + { + if (! $translation instanceof MediaCollectionTranslation) { + return null; + } + + $name = $translation->getAttribute('name'); + if (! is_string($name)) { + return null; + } + + $trimmed = trim($name); + + return $trimmed !== '' ? $trimmed : null; + } + + private static function resolveTitle(Media $media): string + { + if (is_string($media->title) && trim($media->title) !== '') { + return trim($media->title); + } + + return pathinfo((string) $media->file_name, PATHINFO_FILENAME); + } +} diff --git a/packages/demo/src/Seeding/LoadsImageMediaPool.php b/packages/demo/src/Seeding/LoadsImageMediaPool.php new file mode 100644 index 0000000000..3250c513b1 --- /dev/null +++ b/packages/demo/src/Seeding/LoadsImageMediaPool.php @@ -0,0 +1,66 @@ + + */ + protected function loadImageMediaPool(): Collection + { + if (! class_exists(Media::class)) { + return collect(); + } + + $mediaClass = Media::class; + + $query = $mediaClass::query() + ->where(function ($builder): void { + $builder + ->where('mime_type', 'like', 'image/%') + ->orWhereIn('mime_type', [ + 'image/jpeg', + 'image/png', + 'image/webp', + 'image/gif', + 'image/svg+xml', + ]); + }); + + $ids = $query->pluck('id'); + + if ($ids->isEmpty()) { + $ids = $mediaClass::query()->limit(500)->pluck('id'); + } + + if ($ids->isEmpty()) { + return collect(); + } + + return $mediaClass::query()->whereIn('id', $ids)->get(); + } + + /** + * @return array{media_id: int, locale: string}|null + */ + protected function randomImageFieldFromPool(Collection $mediaPool, string $locale): ?array + { + if ($mediaPool->isEmpty()) { + return null; + } + + /** @var Media $media */ + $media = $mediaPool->random(); + + return [ + 'media_id' => (int) $media->getKey(), + 'locale' => $locale, + ]; + } +} diff --git a/packages/demo/src/Seeding/MooxSeederLocales.php b/packages/demo/src/Seeding/MooxSeederLocales.php new file mode 100644 index 0000000000..8fdc119e1d --- /dev/null +++ b/packages/demo/src/Seeding/MooxSeederLocales.php @@ -0,0 +1,69 @@ + */ + public const DEFAULT = ['cs_CZ', 'en_US', 'de_DE', 'pl_PL']; + + public static function isInstalled(): bool + { + return class_exists(DemoServiceProvider::class); + } + + /** + * moox/demo installed → config demo.default_locales; otherwise seeder LOCALES const. + * + * @param list $fallback + * @return list + */ + public static function resolve(array $fallback): array + { + if (self::isInstalled()) { + $configured = config('demo.default_locales', self::DEFAULT); + + if (is_array($configured) && $configured !== []) { + return self::normalizeList($configured); + } + } + + return self::normalizeList($fallback); + } + + /** + * Merges CLI/context locales with config default_locales (DemoLocalizationStep). + * + * @param list $contextLocales + * @return list + */ + public static function mergeForDemoRun(array $contextLocales): array + { + $configured = config('demo.default_locales', self::DEFAULT); + + if (! is_array($configured)) { + $configured = []; + } + + return self::normalizeList(array_merge($contextLocales, $configured)); + } + + /** + * @param array $locales + * @return list + */ + private static function normalizeList(array $locales): array + { + return array_values(array_unique(array_filter( + array_map( + static fn (mixed $locale): string => is_string($locale) ? trim($locale) : '', + $locales, + ), + static fn (string $locale): bool => $locale !== '', + ))); + } +} diff --git a/packages/demo/src/Seeding/ReportsMooxSeederProgress.php b/packages/demo/src/Seeding/ReportsMooxSeederProgress.php new file mode 100644 index 0000000000..1b6c7b45a2 --- /dev/null +++ b/packages/demo/src/Seeding/ReportsMooxSeederProgress.php @@ -0,0 +1,85 @@ + + */ + protected function locales(): array + { + $fallback = defined(static::class.'::LOCALES') + ? constant(static::class.'::LOCALES') + : MooxSeederLocales::DEFAULT; + + if (! is_array($fallback)) { + $fallback = MooxSeederLocales::DEFAULT; + } + + return MooxSeederLocales::resolve($fallback); + } + + protected function hasSeedOutput(): bool + { + return class_exists(SeedOutput::class) && SeedOutput::isBound(); + } + + protected function reportCreated(string $label): void + { + if ($this->hasSeedOutput()) { + SeedOutput::created($label); + + return; + } + + if ($this->command->getOutput()->isVerbose()) { + $this->command->line(" + {$label}"); + } + } + + protected function reportDetail(string $line): void + { + if ($this->hasSeedOutput()) { + SeedOutput::detail($line); + + return; + } + + $this->command->info($line); + } + + /** + * @param list $locales + */ + protected function assertRequiredLocalizations(array $locales): bool + { + if (! class_exists(Localization::class)) { + return true; + } + + $missing = collect($locales) + ->filter( + fn (string $locale): bool => ! Localization::query() + ->where('locale_variant', $locale) + ->exists() + ); + + if ($missing->isEmpty()) { + return true; + } + + $this->command->error( + 'Missing `localizations` rows for: '.$missing->implode(', '). + '. Run moox:demo or add those locale_variant values before running this seeder.' + ); + + return false; + } +} diff --git a/packages/demo/src/Seeding/RunsMooxDemoAssets.php b/packages/demo/src/Seeding/RunsMooxDemoAssets.php new file mode 100644 index 0000000000..55c41fe21e --- /dev/null +++ b/packages/demo/src/Seeding/RunsMooxDemoAssets.php @@ -0,0 +1,25 @@ +setAccessible(true); + $method->invoke($seeder); + } +} diff --git a/packages/demo/src/Seeding/SeedOutput.php b/packages/demo/src/Seeding/SeedOutput.php new file mode 100644 index 0000000000..8ab274fb42 --- /dev/null +++ b/packages/demo/src/Seeding/SeedOutput.php @@ -0,0 +1,47 @@ +created($label); + } + + public static function detail(string $line): void + { + self::$console?->detail($line); + } + + public static function updateTask(string $label): void + { + self::$console?->updateTask($label); + } + + public static function progressBar(int $max, string $message): DemoProgressBar + { + if (self::$console === null) { + throw new \RuntimeException('SeedOutput is not bound to a DemoConsole.'); + } + + return self::$console->progressBar($max, $message); + } +} diff --git a/packages/demo/src/Seeding/SeedingConfig.php b/packages/demo/src/Seeding/SeedingConfig.php new file mode 100644 index 0000000000..7e2ec26310 --- /dev/null +++ b/packages/demo/src/Seeding/SeedingConfig.php @@ -0,0 +1,23 @@ +command->error('User model not available. Install moox/user and run UserSeeder first.'); + + return null; + } + + $author = User::query()->first(); + + if ($author === null) { + $this->command->error('No user found. Run UserSeeder before this seeder.'); + + return null; + } + + return $author; + } + + protected function assignTranslationAuthor(Model $translation, User $author): void + { + $translation->setAttribute('author_id', $author->getKey()); + $translation->setAttribute('author_type', $author->getMorphClass()); + } +} diff --git a/packages/demo/src/Support/MooxPackageDiscovery.php b/packages/demo/src/Support/MooxPackageDiscovery.php new file mode 100644 index 0000000000..41d1b0e8ef --- /dev/null +++ b/packages/demo/src/Support/MooxPackageDiscovery.php @@ -0,0 +1,228 @@ + + */ + public static function mooxPackageNames(?string $basePath = null): array + { + $basePath ??= base_path(); + $names = []; + + $lockPath = $basePath.DIRECTORY_SEPARATOR.'composer.lock'; + if (File::exists($lockPath)) { + $lock = json_decode(File::get($lockPath), true); + $packages = array_merge( + $lock['packages'] ?? [], + $lock['packages-dev'] ?? [] + ); + foreach ($packages as $pkg) { + $name = $pkg['name'] ?? null; + if ($name && str_starts_with($name, 'moox/')) { + $names[$name] = true; + } + } + } + + if ($names === []) { + $composerPath = $basePath.DIRECTORY_SEPARATOR.'composer.json'; + if (File::exists($composerPath)) { + $composer = json_decode(File::get($composerPath), true); + $allPackages = array_merge( + $composer['require'] ?? [], + $composer['require-dev'] ?? [] + ); + foreach (array_keys($allPackages) as $pkg) { + if (str_starts_with($pkg, 'moox/')) { + $names[$pkg] = true; + } + } + } + } + + return array_keys($names); + } + + public static function providerClassForPackage(string $packageName, ?string $basePath = null): ?string + { + $basePath ??= base_path(); + $packageParts = explode('/', $packageName); + $packageDir = $packageParts[1] ?? null; + + if ($packageDir === null) { + return null; + } + + $possiblePaths = [ + $basePath.DIRECTORY_SEPARATOR.'packages'.DIRECTORY_SEPARATOR.$packageDir.DIRECTORY_SEPARATOR.'composer.json', + $basePath.DIRECTORY_SEPARATOR.'vendor'.DIRECTORY_SEPARATOR.$packageName.DIRECTORY_SEPARATOR.'composer.json', + ]; + + foreach ($possiblePaths as $composerPath) { + if (! File::exists($composerPath)) { + continue; + } + + $composer = json_decode(File::get($composerPath), true); + $providerClasses = $composer['extra']['laravel']['providers'] ?? []; + + if (! empty($providerClasses)) { + return $providerClasses[0]; + } + } + + return null; + } + + public static function isMooxProvider(string $providerClass): bool + { + if (! class_exists($providerClass)) { + return false; + } + + try { + $reflection = new \ReflectionClass($providerClass); + + return $reflection->isSubclassOf(MooxServiceProvider::class); + } catch (\Exception) { + return false; + } + } + + /** + * @return array package name => provider class + */ + public static function scanMooxProviders(?string $basePath = null): array + { + $basePath ??= base_path(); + $result = []; + + foreach (self::mooxPackageNames($basePath) as $packageName) { + $providerClass = self::providerClassForPackage($packageName, $basePath); + + if (! $providerClass || ! self::isMooxProvider($providerClass)) { + continue; + } + + $result[$packageName] = $providerClass; + } + + return $result; + } + + public static function composerPathForPackage(string $packageName, ?string $basePath = null): ?string + { + $basePath ??= base_path(); + $packageParts = explode('/', $packageName); + $packageDir = $packageParts[1] ?? null; + + if ($packageDir === null) { + return null; + } + + $possiblePaths = [ + $basePath.DIRECTORY_SEPARATOR.'packages'.DIRECTORY_SEPARATOR.$packageDir.DIRECTORY_SEPARATOR.'composer.json', + $basePath.DIRECTORY_SEPARATOR.'vendor'.DIRECTORY_SEPARATOR.$packageName.DIRECTORY_SEPARATOR.'composer.json', + ]; + + foreach ($possiblePaths as $composerPath) { + if (File::exists($composerPath)) { + return $composerPath; + } + } + + return null; + } + + /** + * @return array|null + */ + public static function readComposerJson(string $packageName, ?string $basePath = null): ?array + { + $composerPath = self::composerPathForPackage($packageName, $basePath); + + if ($composerPath === null) { + return null; + } + + $composer = json_decode(File::get($composerPath), true); + + return is_array($composer) ? $composer : null; + } + + public static function packageSlug(string $packageName): string + { + $parts = explode('/', $packageName); + + return $parts[1] ?? $packageName; + } + + public static function resolveSeederClass(string $packageName, string $seeder): ?string + { + if (class_exists($seeder)) { + return $seeder; + } + + $packageNamespace = 'Moox\\'.ucfirst(self::packageSlug($packageName)); + + $possibleClasses = [ + $packageNamespace.'\\Database\\Seeders\\'.ucfirst($seeder), + $packageNamespace.'\\Database\\Seeders\\'.$seeder, + $packageNamespace.'\\Seeders\\'.ucfirst($seeder), + $packageNamespace.'\\Seeders\\'.$seeder, + ]; + + $composer = self::readComposerJson($packageName); + $seedPath = $composer['extra']['moox']['install']['seed'] ?? null; + if (is_string($seedPath) && $seedPath !== '') { + $basename = basename($seedPath, '.php'); + $possibleClasses[] = $packageNamespace.'\\Database\\Seeders\\'.ucfirst($basename); + $possibleClasses[] = $packageNamespace.'\\Database\\Seeders\\'.$basename; + } + + foreach ($possibleClasses as $class) { + if (class_exists($class)) { + return $class; + } + } + + return null; + } + + /** + * @return array> slug => list of moox/* dependency slugs + */ + public static function mooxDependencyGraph(?string $basePath = null): array + { + $basePath ??= base_path(); + $graph = []; + + foreach (self::mooxPackageNames($basePath) as $packageName) { + $slug = self::packageSlug($packageName); + $composer = self::readComposerJson($packageName, $basePath); + $deps = []; + + if (is_array($composer)) { + foreach (['require', 'require-dev'] as $section) { + foreach ($composer[$section] ?? [] as $name => $_constraint) { + if (is_string($name) && str_starts_with($name, 'moox/')) { + $deps[] = self::packageSlug($name); + } + } + } + } + + $graph[$slug] = array_values(array_unique($deps)); + } + + return $graph; + } +} diff --git a/packages/demo/tests/CategorySeederLocaleTextTest.php b/packages/demo/tests/CategorySeederLocaleTextTest.php new file mode 100644 index 0000000000..149b5ae353 --- /dev/null +++ b/packages/demo/tests/CategorySeederLocaleTextTest.php @@ -0,0 +1,49 @@ +fakerLocaleTitle('de_DE', $faker, 'title'); + $description = $this->fakerLocaleText('de_DE', $faker, preset: 'description'); + $content = $this->markdownContentFromLocale('de_DE', $faker); + + $bundle = $title.' '.$description.' '.$content; + + $this->assertNotEmpty($title); + $this->assertDoesNotMatchRegularExpression( + '/\b(dolorem|voluptat|architecto|lorem|aperiam)\b/i', + $bundle, + 'Category-style de_DE fields must not use Lorem ipsum tokens', + ); + } + } + + public function test_missing_real_text_throws_for_moox_locales(): void + { + $faker = new class extends Generator + { + public function __construct() {} + }; + + $this->expectException(\RuntimeException::class); + $this->fakerLocaleText('de_DE', $faker, preset: 'description'); + } +} diff --git a/packages/demo/tests/FakerLocaleTextSmokeTest.php b/packages/demo/tests/FakerLocaleTextSmokeTest.php new file mode 100644 index 0000000000..fdbc3abe68 --- /dev/null +++ b/packages/demo/tests/FakerLocaleTextSmokeTest.php @@ -0,0 +1,40 @@ +fakerLocaleTitle('de_DE', $faker, 'title'); + $description = $this->fakerLocaleText('de_DE', $faker, preset: 'description'); + + $this->assertNotEmpty($title); + $this->assertNotEmpty($description); + $this->assertDoesNotMatchRegularExpression( + '/\b(dolorem|voluptat|architecto|lorem)\b/i', + $title.' '.$description, + 'de_DE demo text should not look like Lorem ipsum', + ); + } + } + + public function test_locale_supports_real_text(): void + { + $faker = Factory::create('de_DE'); + + $this->assertTrue($this->localeSupportsRealText($faker)); + $this->assertIsString($this->fakerLocaleText('de_DE', $faker, 50, 100)); + } +} diff --git a/packages/demo/tests/Unit/DemoContextTest.php b/packages/demo/tests/Unit/DemoContextTest.php new file mode 100644 index 0000000000..79c820fa0e --- /dev/null +++ b/packages/demo/tests/Unit/DemoContextTest.php @@ -0,0 +1,28 @@ +assertSame(100, $context->datasetCount); + $this->assertSame(['de_DE', 'en_US'], $context->locales); + } +} diff --git a/packages/demo/tests/Unit/SeederOrderResolverTest.php b/packages/demo/tests/Unit/SeederOrderResolverTest.php new file mode 100644 index 0000000000..8051feb013 --- /dev/null +++ b/packages/demo/tests/Unit/SeederOrderResolverTest.php @@ -0,0 +1,30 @@ +resolve(); + + $slugs = array_column($ordered, 'slug'); + + if (! in_array('data', $slugs, true) || ! in_array('category', $slugs, true)) { + $this->markTestSkipped('moox/data or moox/category not in resolved providers for this environment.'); + } + + $dataIndex = array_search('data', $slugs, true); + $categoryIndex = array_search('category', $slugs, true); + + $this->assertNotFalse($dataIndex); + $this->assertNotFalse($categoryIndex); + $this->assertLessThan($categoryIndex, $dataIndex); + } +} diff --git a/packages/draft/composer.json b/packages/draft/composer.json index 627be6713c..faf8951e54 100644 --- a/packages/draft/composer.json +++ b/packages/draft/composer.json @@ -27,7 +27,8 @@ "autoload": { "psr-4": { "Moox\\Draft\\": "src", - "Moox\\Draft\\Database\\Factories\\": "database/factories" + "Moox\\Draft\\Database\\Factories\\": "database/factories", + "Moox\\Draft\\Database\\Seeders\\": "database/seeders" } }, "autoload-dev": { diff --git a/packages/draft/database/factories/DraftFactory.php b/packages/draft/database/factories/DraftFactory.php index 07db20117c..fad41b652b 100644 --- a/packages/draft/database/factories/DraftFactory.php +++ b/packages/draft/database/factories/DraftFactory.php @@ -3,8 +3,13 @@ namespace Moox\Draft\Database\Factories; use Illuminate\Database\Eloquent\Factories\Factory; +use Illuminate\Support\Carbon; use Moox\Draft\Models\Draft; +use Moox\Draft\Models\DraftTranslation; +/** + * @extends Factory + */ class DraftFactory extends Factory { protected $model = Draft::class; @@ -146,8 +151,14 @@ public function published(): static })->afterCreating(function (Draft $draft) { // Override translation status for published foreach ($draft->translations as $translation) { + if (! $translation instanceof DraftTranslation) { + continue; + } + $translation->translation_status = 'published'; - $translation->published_at = $this->faker->dateTimeBetween('-30 days', 'now'); + $translation->published_at = Carbon::instance( + $this->faker->dateTimeBetween('-30 days', 'now') + ); $translation->save(); } }); @@ -165,8 +176,14 @@ public function scheduled(): static })->afterCreating(function (Draft $draft) { // Override translation status for scheduled foreach ($draft->translations as $translation) { + if (! $translation instanceof DraftTranslation) { + continue; + } + $translation->translation_status = 'scheduled'; - $translation->to_publish_at = $this->faker->dateTimeBetween('now', '+7 days'); + $translation->to_publish_at = Carbon::instance( + $this->faker->dateTimeBetween('now', '+7 days') + ); $translation->save(); } }); diff --git a/packages/draft/database/seeders/DraftSeeder.php b/packages/draft/database/seeders/DraftSeeder.php new file mode 100644 index 0000000000..8d49f02724 --- /dev/null +++ b/packages/draft/database/seeders/DraftSeeder.php @@ -0,0 +1,184 @@ + */ + private const TYPES = ['article', 'page', 'post', 'news', 'tutorial']; + + /** @var list */ + private const TRANSLATION_STATUSES = ['draft', 'waiting', 'private', 'scheduled', 'published']; + + private const MEDIA_ATTACH_PROBABILITY = 0.75; + + private const PROGRESS_LOG_EVERY = 100; + + public function run(): void + { + $this->seed(); + + if (class_exists(RunsMooxDemoAssets::class)) { + RunsMooxDemoAssets::invoke($this); + } + } + + protected function seed(): void + { + if (! $this->assertRequiredLocalizations($this->locales())) { + return; + } + + $this->purgeDemoDrafts(); + + $author = $this->requireDemoAuthor(); + if ($author === null) { + return; + } + + $count = $this->resolveDraftCount(); + $faker = fake(); + $baseUrl = rtrim((string) config('app.url'), '/'); + $mediaPool = $this->loadImageMediaPool(); + $created = 0; + + if ($mediaPool->isEmpty()) { + $this->command->warn('No images in `media` table — drafts will be seeded without mediathek images.'); + } + + $progress = $this->hasSeedOutput() + ? SeedOutput::progressBar($count, 'Demo drafts') + : null; + + DB::transaction(function () use ($count, $faker, $author, $baseUrl, $mediaPool, $progress, &$created): void { + for ($index = 1; $index <= $count; $index++) { + $status = $faker->randomElement(self::TRANSLATION_STATUSES); + $contentLocale = $this->locales()[array_rand($this->locales())]; + $image = $this->resolveDraftImage($faker, $mediaPool, $contentLocale); + + $draft = Draft::query()->create([ + 'is_active' => $faker->boolean(85), + 'type' => $faker->randomElement(self::TYPES), + 'color' => $faker->hexColor(), + 'status' => $status, + 'due_at' => $faker->optional(0.4)->dateTimeBetween('now', '+45 days'), + 'image' => $image, + 'data' => json_encode([ + 'seed_source' => 'draft_seeder_v1', + 'seed_index' => $index, + ], JSON_THROW_ON_ERROR), + ]); + + foreach ($this->locales() as $locale) { + $localeFaker = $this->fakerForLocale($locale); + $title = $this->formatFakerWords($locale, $localeFaker, 3, 7); + $slug = self::DEMO_SLUG_PREFIX + .'-'.Str::slug($title) + .'-'.Str::lower($locale) + .'-'.sprintf('%04d', $index); + + $translation = $draft->translateOrNew($locale); + $translation->title = $title; + $translation->slug = Str::limit($slug, 180, ''); + $translation->permalink = $baseUrl.'/'.$locale.'/'.$translation->slug; + $translation->description = $this->fakerLocaleText($locale, $localeFaker, preset: 'description'); + $translation->content = implode("\n\n", $this->fakerLocaleParagraphs( + $locale, + $localeFaker, + 3, + 6, + )); + $translation->translation_status = $status; + + $this->assignTranslationAuthor($translation, $author); + } + + $draft->save(); + $created++; + + if ($progress !== null) { + $progress->advance(); + } elseif ($index % self::PROGRESS_LOG_EVERY === 0 || $index === $count) { + $this->reportCreated("Draft {$draft->getKey()}"); + } + } + }); + + $progress?->finish("{$count} demo draft(s)"); + + $this->reportDetail(sprintf( + '%d faker draft(s) seeded with %d locale(s) each.', + $created, + count($this->locales()) + )); + } + + /** + * @return array{media_id: int, locale: string}|null + */ + private function resolveDraftImage(Generator $faker, Collection $mediaPool, string $locale): ?array + { + if ($mediaPool->isNotEmpty() && $faker->boolean((int) (self::MEDIA_ATTACH_PROBABILITY * 100))) { + return $this->randomImageFieldFromPool($mediaPool, $locale); + } + + return null; + } + + private function purgeDemoDrafts(): void + { + Draft::query() + ->whereHas('translations', function ($query): void { + $query->where('slug', 'like', self::DEMO_SLUG_PREFIX.'-%'); + }) + ->forceDelete(); + } + + private function resolveDraftCount(): int + { + if (class_exists(SeedingConfig::class)) { + return SeedingConfig::resolveCount('draft', self::DEFAULT_DRAFT_COUNT); + } + + return self::DEFAULT_DRAFT_COUNT; + } + + private function fakerForLocale(string $locale): Generator + { + static $cache = []; + $resolvedLocale = in_array($locale, $this->locales(), true) ? $locale : 'en_US'; + + if (! isset($cache[$resolvedLocale])) { + $cache[$resolvedLocale] = FakerFactory::create($resolvedLocale); + } + + return $cache[$resolvedLocale]; + } +} diff --git a/packages/item/composer.json b/packages/item/composer.json index 956eb4ac3e..41474b3759 100644 --- a/packages/item/composer.json +++ b/packages/item/composer.json @@ -23,7 +23,8 @@ "autoload": { "psr-4": { "Moox\\Item\\": "src", - "Moox\\Item\\Database\\Factories\\": "database/factories" + "Moox\\Item\\Database\\Factories\\": "database/factories", + "Moox\\Item\\Database\\Seeders\\": "database/seeders" } }, "autoload-dev": { @@ -46,6 +47,7 @@ "type": "moox-plugin", "install": { "auto_migrate": "database/migrations", + "seed": "database/seeders/ItemSeeder.php", "auto_entities": { "Item": true }, diff --git a/packages/item/database/seeders/ItemSeeder.php b/packages/item/database/seeders/ItemSeeder.php new file mode 100644 index 0000000000..4526ee4f63 --- /dev/null +++ b/packages/item/database/seeders/ItemSeeder.php @@ -0,0 +1,120 @@ +seed(); + + if (class_exists(RunsMooxDemoAssets::class)) { + RunsMooxDemoAssets::invoke($this); + } + } + + protected function seed(): void + { + if (! $this->assertRequiredLocalizations($this->locales())) { + return; + } + + $this->purgeDemoItems(); + + $faker = fake(); + $count = $this->resolveItemCount(); + $created = 0; + + $progress = $this->hasSeedOutput() + ? SeedOutput::progressBar($count, 'Demo items') + : null; + + DB::transaction(function () use ($count, $faker, $progress, &$created): void { + for ($index = 1; $index <= $count; $index++) { + $locale = $this->locales()[array_rand($this->locales())]; + $localeFaker = $this->fakerForLocale($locale); + $title = $this->formatFakerWords($locale, $localeFaker, 2, 5); + + $item = Item::query()->create([ + 'title' => $title, + 'description' => $this->fakerLocaleText($locale, $localeFaker, preset: 'description'), + 'custom_properties' => [ + 'seed_source' => 'item_seeder_v1', + 'seed_index' => $index, + 'seed_locale' => $locale, + 'seed_key' => Str::slug($title).'-'.sprintf('%04d', $index), + 'is_featured' => $faker->boolean(25), + ], + ]); + + $created++; + + if ($progress !== null) { + $progress->advance(); + } elseif ($index % self::PROGRESS_LOG_EVERY === 0 || $index === $count) { + $this->reportCreated("Item {$item->getKey()}"); + } + } + }); + + $progress?->finish("{$count} demo item(s)"); + + $this->reportDetail(sprintf( + '%d faker item(s) seeded (one random locale per item from %d configured locale(s)).', + $created, + count($this->locales()) + )); + } + + private function purgeDemoItems(): void + { + Item::query() + ->where('custom_properties->seed_source', 'item_seeder_v1') + ->delete(); + } + + private function resolveItemCount(): int + { + if (class_exists(SeedingConfig::class)) { + return SeedingConfig::resolveCount('item', self::DEFAULT_ITEM_COUNT); + } + + return self::DEFAULT_ITEM_COUNT; + } + + private function fakerForLocale(string $locale): Generator + { + static $cache = []; + $resolvedLocale = in_array($locale, $this->locales(), true) ? $locale : 'en_US'; + + if (! isset($cache[$resolvedLocale])) { + $cache[$resolvedLocale] = FakerFactory::create($resolvedLocale); + } + + return $cache[$resolvedLocale]; + } +} diff --git a/packages/product/composer.json b/packages/product/composer.json index abeadc7c37..ec10985d63 100644 --- a/packages/product/composer.json +++ b/packages/product/composer.json @@ -52,6 +52,7 @@ "type": "moox-plugin", "install": { "auto_migrate": "database/migrations", + "seed": "database/seeders/ProductSeeder.php", "auto_publish": "moox-product-config", "auto_entities": { "Product": true diff --git a/packages/product/database/factories/ProductFactory.php b/packages/product/database/factories/ProductFactory.php index 26e2237f4c..0872a3f833 100644 --- a/packages/product/database/factories/ProductFactory.php +++ b/packages/product/database/factories/ProductFactory.php @@ -59,9 +59,6 @@ public function withoutTranslations(): static }); } - /** - * @param list $locales - */ public function withTranslationLocales(string ...$locales): static { $locales = array_values(array_unique($locales)); @@ -141,12 +138,12 @@ protected function defaultTranslatedAttributes(): array 'title' => $title, 'slug' => $slug, 'permalink' => 'https://example.test/products/'.$slug, - 'subtitle' => $this->faker->optional(0.7)->sentence(4) ?? '', - 'excerpt' => $this->faker->optional(0.8)->text(180) ?? '', + 'subtitle' => $this->faker->optional(0.7)->sentence(4) ?: '', + 'excerpt' => $this->faker->optional(0.8)->text(180) ?: '', 'description' => $this->faker->paragraph(), 'content' => $this->faker->paragraphs(2, true), - 'meta_title' => $this->faker->optional(0.6)->sentence(6) ?? '', - 'meta_description' => $this->faker->optional(0.6)->text(140) ?? '', + 'meta_title' => $this->faker->optional(0.6)->sentence(6) ?: '', + 'meta_description' => $this->faker->optional(0.6)->text(140) ?: '', ]; } } diff --git a/packages/product/database/seeders/ProductSeeder.php b/packages/product/database/seeders/ProductSeeder.php new file mode 100644 index 0000000000..75714356a7 --- /dev/null +++ b/packages/product/database/seeders/ProductSeeder.php @@ -0,0 +1,199 @@ + */ + private const PRODUCT_TYPES = ['simple', 'configurable', 'bundle', 'digital']; + + /** @var list */ + private const PRODUCT_STATUSES = ['draft', 'waiting', 'private', 'scheduled', 'published']; + + private const MEDIA_ATTACH_PROBABILITY = 0.75; + + private const PROGRESS_LOG_EVERY = 100; + + public function run(): void + { + $this->seed(); + + if (class_exists(RunsMooxDemoAssets::class)) { + RunsMooxDemoAssets::invoke($this); + } + } + + protected function seed(): void + { + if (! $this->assertRequiredLocalizations($this->locales())) { + return; + } + + $this->purgeDemoProducts(); + + $author = $this->requireDemoAuthor(); + if ($author === null) { + return; + } + + $faker = fake(); + $count = $this->resolveProductCount(); + $baseUrl = rtrim((string) config('app.url'), '/'); + $mediaPool = $this->loadImageMediaPool(); + + if ($mediaPool->isEmpty()) { + $this->command->warn('No images in `media` table — products will be seeded without mediathek images.'); + } + + $created = 0; + $progress = $this->hasSeedOutput() + ? SeedOutput::progressBar($count, 'Demo products') + : null; + + DB::transaction(function () use ($count, $faker, $author, $baseUrl, $mediaPool, $progress, &$created): void { + for ($index = 1; $index <= $count; $index++) { + $status = $faker->randomElement(self::PRODUCT_STATUSES); + $sku = 'DEMO-'.strtoupper($faker->unique()->bothify('??###??')); + $imageLocale = $this->locales()[array_rand($this->locales())]; + + $product = Product::query()->create([ + 'is_active' => $faker->boolean(88), + 'image' => $this->resolveProductImage($faker, $mediaPool, $imageLocale), + 'type' => $faker->randomElement(self::PRODUCT_TYPES), + 'due_at' => $faker->optional(0.3)->dateTimeBetween('now', '+90 days'), + 'color' => $faker->hexColor(), + 'sku' => $sku, + 'gtin' => $faker->optional(0.6)->numerify('##############'), + 'mpn' => $faker->optional(0.7)->bothify('MPN-####-??'), + 'brand_name' => null, + 'weight_grams' => $faker->numberBetween(100, 25000), + 'length_mm' => $faker->numberBetween(50, 1500), + 'width_mm' => $faker->numberBetween(50, 1200), + 'height_mm' => $faker->numberBetween(20, 1000), + 'status' => $status, + 'custom_properties' => [ + 'seed_source' => 'product_seeder_v1', + 'seed_index' => $index, + 'featured' => $faker->boolean(20), + ], + ]); + + foreach ($this->locales() as $locale) { + $localeFaker = $this->fakerForLocale($locale); + $title = $this->formatFakerWords($locale, $localeFaker, 2, 5); + $slug = self::DEMO_SLUG_PREFIX + .'-'.Str::slug($title) + .'-'.Str::lower($locale) + .'-'.sprintf('%04d', $index); + + $translation = $product->translateOrNew($locale); + $translation->title = $title; + $translation->slug = Str::limit($slug, 180, ''); + $translation->permalink = $baseUrl.'/'.$locale.'/products/'.$translation->slug; + $translation->subtitle = $this->fakerLocaleSentence($locale, $localeFaker, 20, 50); + $translation->excerpt = $this->fakerLocaleText($locale, $localeFaker, preset: 'excerpt', limit: 90); + $translation->description = $this->fakerLocaleText($locale, $localeFaker, preset: 'description'); + $translation->content = implode("\n\n", $this->fakerLocaleParagraphs( + $locale, + $localeFaker, + 2, + 5, + )); + $translation->meta_title = $title.' - '.$localeFaker->company(); + $translation->meta_description = Str::limit($translation->description ?? '', 140, ''); + $translation->translation_status = $status; + + $this->assignTranslationAuthor($translation, $author); + } + + $product->save(); + $created++; + + if ($progress !== null) { + $progress->advance(); + } elseif ($index % self::PROGRESS_LOG_EVERY === 0 || $index === $count) { + $this->reportCreated("Product {$product->getKey()}"); + } + } + }); + + $progress?->finish("{$count} demo product(s)"); + + $this->reportDetail(sprintf( + '%d faker product(s) seeded with %d locale(s) each (Locale-Lock per translation).', + $created, + count($this->locales()) + )); + } + + /** + * @param Collection $mediaPool + * @return array|null + */ + private function resolveProductImage(Generator $faker, Collection $mediaPool, string $locale): ?array + { + if ($mediaPool->isNotEmpty() && $faker->boolean((int) (self::MEDIA_ATTACH_PROBABILITY * 100))) { + return $this->randomImageFieldFromPool($mediaPool, $locale); + } + + return null; + } + + private function purgeDemoProducts(): void + { + Product::query() + ->whereHas('translations', function ($query): void { + $query->where('slug', 'like', self::DEMO_SLUG_PREFIX.'-%'); + }) + ->forceDelete(); + } + + private function resolveProductCount(): int + { + if (class_exists(SeedingConfig::class)) { + return SeedingConfig::resolveCount('product', self::DEFAULT_PRODUCT_COUNT); + } + + return self::DEFAULT_PRODUCT_COUNT; + } + + private function fakerForLocale(string $locale): Generator + { + static $cache = []; + $resolvedLocale = in_array($locale, $this->locales(), true) ? $locale : 'en_US'; + + if (! isset($cache[$resolvedLocale])) { + $cache[$resolvedLocale] = FakerFactory::create($resolvedLocale); + } + + return $cache[$resolvedLocale]; + } +} diff --git a/packages/record/composer.json b/packages/record/composer.json index 8bd73319a1..392883c2a2 100644 --- a/packages/record/composer.json +++ b/packages/record/composer.json @@ -23,7 +23,8 @@ "autoload": { "psr-4": { "Moox\\Record\\": "src", - "Moox\\Record\\Database\\Factories\\": "database/factories" + "Moox\\Record\\Database\\Factories\\": "database/factories", + "Moox\\Record\\Database\\Seeders\\": "database/seeders" } }, "extra": { @@ -33,7 +34,10 @@ ] }, "moox": { - "stability": "stable" + "stability": "stable", + "install": { + "seed": "database/seeders/RecordSeeder.php" + } } }, "minimum-stability": "stable", diff --git a/packages/record/database/seeders/RecordSeeder.php b/packages/record/database/seeders/RecordSeeder.php new file mode 100644 index 0000000000..09647a4faf --- /dev/null +++ b/packages/record/database/seeders/RecordSeeder.php @@ -0,0 +1,141 @@ +seed(); + + if (class_exists(RunsMooxDemoAssets::class)) { + RunsMooxDemoAssets::invoke($this); + } + } + + protected function seed(): void + { + if (! $this->assertRequiredLocalizations($this->locales())) { + return; + } + + $this->purgeDemoRecords(); + + $author = $this->requireDemoAuthor(); + if ($author === null) { + return; + } + + $count = $this->resolveRecordCount(); + $faker = fake(); + $baseUrl = rtrim((string) config('app.url'), '/'); + $created = 0; + + $progress = $this->hasSeedOutput() + ? SeedOutput::progressBar($count, 'Demo records') + : null; + + DB::transaction(function () use ($count, $author, $faker, $baseUrl, $progress, &$created): void { + for ($index = 1; $index <= $count; $index++) { + $locale = $this->locales()[array_rand($this->locales())]; + $localeFaker = $this->fakerForLocale($locale); + $title = $this->formatFakerWords($locale, $localeFaker, 2, 6); + $slug = self::DEMO_SLUG_PREFIX + .'-'.Str::slug($title) + .'-'.Str::lower($locale) + .'-'.sprintf('%04d', $index); + $status = $faker->randomElement([ + RecordStatus::ACTIVE->value, + RecordStatus::INACTIVE->value, + RecordStatus::ARCHIVED->value, + ]); + + $record = Record::query()->create([ + 'title' => $title, + 'slug' => Str::limit($slug, 180, ''), + 'description' => $this->fakerLocaleText($locale, $localeFaker, preset: 'description'), + 'permalink' => $baseUrl.'/'.$locale.'/'.$slug, + 'status' => $status, + 'custom_properties' => [ + 'seed_source' => 'record_seeder_v1', + 'seed_index' => $index, + 'seed_locale' => $locale, + ], + 'author_id' => $author->getKey(), + 'author_type' => $author->getMorphClass(), + ]); + + $created++; + + if ($progress !== null) { + $progress->advance(); + } elseif ($index % self::PROGRESS_LOG_EVERY === 0 || $index === $count) { + $this->reportCreated("Record {$record->getKey()}"); + } + } + }); + + $progress?->finish("{$count} demo record(s)"); + + $this->reportDetail(sprintf( + '%d faker record(s) seeded (one random locale per record from %d configured locale(s)).', + $created, + count($this->locales()) + )); + } + + private function purgeDemoRecords(): void + { + Record::query() + ->where('slug', 'like', self::DEMO_SLUG_PREFIX.'-%') + ->forceDelete(); + } + + private function resolveRecordCount(): int + { + if (class_exists(SeedingConfig::class)) { + return SeedingConfig::resolveCount('record', self::DEFAULT_RECORD_COUNT); + } + + return self::DEFAULT_RECORD_COUNT; + } + + private function fakerForLocale(string $locale): Generator + { + static $cache = []; + $resolvedLocale = in_array($locale, $this->locales(), true) ? $locale : 'en_US'; + + if (! isset($cache[$resolvedLocale])) { + $cache[$resolvedLocale] = FakerFactory::create($resolvedLocale); + } + + return $cache[$resolvedLocale]; + } +} diff --git a/packages/tag/composer.json b/packages/tag/composer.json index 4c34240b9d..bd9a20ad2c 100644 --- a/packages/tag/composer.json +++ b/packages/tag/composer.json @@ -24,7 +24,8 @@ "autoload": { "psr-4": { "Moox\\Tag\\": "src/", - "Moox\\Tag\\Database\\Factories\\": "database/factories" + "Moox\\Tag\\Database\\Factories\\": "database/factories", + "Moox\\Tag\\Database\\Seeders\\": "database/seeders" } }, "extra": { @@ -34,7 +35,10 @@ ] }, "moox": { - "stability": "stable" + "stability": "stable", + "install": { + "seed": "database/seeders/TagSeeder.php" + } } }, "prefer-stable": true, diff --git a/packages/tag/database/factories/TagFactory.php b/packages/tag/database/factories/TagFactory.php index cf68d5caa6..77aae896cb 100644 --- a/packages/tag/database/factories/TagFactory.php +++ b/packages/tag/database/factories/TagFactory.php @@ -10,6 +10,9 @@ use Moox\Tag\Models\Tag; use Moox\Tag\Models\TagTranslation; +/** + * @extends Factory + */ class TagFactory extends Factory { protected $model = Tag::class; diff --git a/packages/tag/database/seeders/TagSeeder.php b/packages/tag/database/seeders/TagSeeder.php index bef00e1243..4364a120eb 100644 --- a/packages/tag/database/seeders/TagSeeder.php +++ b/packages/tag/database/seeders/TagSeeder.php @@ -1,46 +1,185 @@ */ + private const TAG_STATUSES = ['draft', 'waiting', 'private', 'scheduled', 'published']; + + private const MEDIA_ATTACH_PROBABILITY = 0.8; + + private const PROGRESS_LOG_EVERY = 100; + public function run(): void { - // Create system tags with all translations - Tag::factory() - ->count(3) - ->system() - ->withAllTranslations() - ->create(); - - // Create featured tags with specific translations - Tag::factory() - ->count(5) - ->featured() - ->withGermanTranslation() - ->withFrenchTranslation() - ->create(); - - // Create tags with random translations - Tag::factory() - ->count(10) - ->withRandomTranslations(3) - ->create(); - - // Create tags with specific language combinations - Tag::factory() - ->count(2) - ->withSpanishTranslation() - ->withItalianTranslation() - ->create(); - - // Create Dutch-only tags - Tag::factory() - ->count(2) - ->withDutchTranslation() - ->create(); + $this->seed(); + + if (class_exists(RunsMooxDemoAssets::class)) { + RunsMooxDemoAssets::invoke($this); + } + } + + protected function seed(): void + { + if (! $this->assertRequiredLocalizations($this->locales())) { + return; + } + + $this->purgeDemoTags(); + + $author = $this->requireDemoAuthor(); + if ($author === null) { + return; + } + + $faker = fake(); + $count = $this->resolveTagCount(); + $baseUrl = rtrim((string) config('app.url'), '/'); + $mediaPool = $this->loadImageMediaPool(); + $created = 0; + $withMedia = 0; + + if ($mediaPool->isEmpty()) { + $this->command->warn('No images in media table - tags will be seeded without media_usables.'); + } + + $progress = $this->hasSeedOutput() + ? SeedOutput::progressBar($count, 'Demo tags') + : null; + + DB::transaction(function () use ($count, $faker, $author, $baseUrl, $mediaPool, $progress, &$created, &$withMedia): void { + for ($index = 1; $index <= $count; $index++) { + $status = $faker->randomElement(self::TAG_STATUSES); + + $tag = Tag::query()->create([ + 'is_active' => $faker->boolean(85), + 'color' => $faker->hexColor(), + 'weight' => $faker->numberBetween(1, 10), + 'count' => $faker->numberBetween(0, 100), + 'status' => $status, + 'due_at' => $faker->optional(0.25)->dateTimeBetween('now', '+60 days'), + 'custom_properties' => [ + 'seed_source' => 'tag_seeder_v1', + 'seed_index' => $index, + ], + ]); + + foreach ($this->locales() as $locale) { + $localeFaker = $this->fakerForLocale($locale); + $title = $this->formatFakerWords($locale, $localeFaker, 1, 3); + $slug = self::DEMO_SLUG_PREFIX + .'-'.Str::slug($title) + .'-'.Str::lower($locale) + .'-'.sprintf('%04d', $index); + + $translation = $tag->translateOrNew($locale); + $translation->title = $title; + $translation->slug = Str::limit($slug, 180, ''); + $translation->permalink = $baseUrl.'/'.$locale.'/'.$translation->slug; + $translation->description = $this->fakerLocaleText($locale, $localeFaker, preset: 'description'); + $translation->content = implode("\n\n", $this->fakerLocaleParagraphs( + $locale, + $localeFaker, + 2, + 4, + )); + $translation->translation_status = $status; + + $this->assignTranslationAuthor($translation, $author); + } + + $tag->save(); + + if ($mediaPool->isNotEmpty() && $faker->boolean((int) (self::MEDIA_ATTACH_PROBABILITY * 100))) { + /** @var Media $media */ + $media = $mediaPool->random(); + + DB::table('media_usables')->insertOrIgnore([ + 'media_id' => $media->getKey(), + 'media_usable_id' => $tag->getKey(), + 'media_usable_type' => Tag::class, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $withMedia++; + } + + $created++; + + if ($progress !== null) { + $progress->advance(); + } elseif ($index % self::PROGRESS_LOG_EVERY === 0 || $index === $count) { + $this->reportCreated("Tag {$tag->getKey()}"); + } + } + }); + + $progress?->finish("{$count} demo tag(s)"); + + $this->reportDetail(sprintf( + '%d faker tag(s) seeded with %d locale(s) each, %d with media link(s).', + $created, + count($this->locales()), + $withMedia + )); + } + + private function purgeDemoTags(): void + { + Tag::query() + ->whereHas('translations', function ($query): void { + $query->where('slug', 'like', self::DEMO_SLUG_PREFIX.'-%'); + }) + ->forceDelete(); + } + + private function resolveTagCount(): int + { + if (class_exists(SeedingConfig::class)) { + return SeedingConfig::resolveCount('tag', self::DEFAULT_TAG_COUNT); + } + + return self::DEFAULT_TAG_COUNT; + } + + private function fakerForLocale(string $locale): Generator + { + static $cache = []; + $resolvedLocale = in_array($locale, $this->locales(), true) ? $locale : 'en_US'; + + if (! isset($cache[$resolvedLocale])) { + $cache[$resolvedLocale] = FakerFactory::create($resolvedLocale); + } + + return $cache[$resolvedLocale]; } } diff --git a/packages/user/composer.json b/packages/user/composer.json index 2d4ecb973c..c92df8cb08 100644 --- a/packages/user/composer.json +++ b/packages/user/composer.json @@ -17,14 +17,16 @@ } ], "require": { - "moox/core": "dev-main" + "moox/core": "dev-main", + "moox/media": "dev-main" }, "require-dev": { "moox/devtools": "dev-main" }, "autoload": { "psr-4": { - "Moox\\User\\": "src" + "Moox\\User\\": "src", + "Moox\\User\\Database\\Seeders\\": "database/seeders" } }, "extra": { @@ -34,7 +36,10 @@ ] }, "moox": { - "stability": "stable" + "stability": "stable", + "install": { + "seed": "database/seeders/UserSeeder.php" + } } }, "prefer-stable": true diff --git a/packages/user/database/seeders/UserSeeder.php b/packages/user/database/seeders/UserSeeder.php new file mode 100644 index 0000000000..a166626118 --- /dev/null +++ b/packages/user/database/seeders/UserSeeder.php @@ -0,0 +1,256 @@ + + */ + public const DEFAULT_USERS = [ + [ + 'name' => 'Reinhold Jesse', + 'email' => 'reinhold.jesse@heco.de', + 'password' => '123456789', + ], + [ + 'name' => 'Moox Admin', + 'email' => 'admin@moox.org', + 'password' => 'password', + ], + [ + 'name' => 'Moox Editor', + 'email' => 'editor@moox.org', + 'password' => 'password', + ], + ]; + + /** Fallback when moox/demo is not installed; otherwise {@see locales()}. */ + public const LOCALES = ['cs_CZ', 'en_US', 'de_DE', 'pl_PL']; + + public function run(): void + { + $this->seed(); + + if (class_exists(RunsMooxDemoAssets::class)) { + RunsMooxDemoAssets::invoke($this); + } + } + + protected function seed(): void + { + $extraCount = $this->resolveExtraUserCount(); + + $this->purgeDemoUsers(); + + $seededFixed = 0; + + foreach (self::DEFAULT_USERS as $user) { + User::query()->create([ + 'name' => $user['name'], + 'email' => $user['email'], + 'password' => Hash::make($user['password']), + 'email_verified_at' => now(), + ]); + + $seededFixed++; + $this->reportCreated("User {$user['email']}"); + } + + if ($extraCount > 0) { + $this->seedExtraUsers($extraCount); + } + + $total = $seededFixed + $extraCount; + + $this->reportDetail(sprintf( + '%d user(s) total (%d default account(s) + %d from dataset)', + $total, + $seededFixed, + $extraCount + )); + } + + private function seedExtraUsers(int $extraCount): void + { + if ($this->hasSeedOutput()) { + $progress = SeedOutput::progressBar($extraCount, 'Demo users'); + + for ($i = 1; $i <= $extraCount; $i++) { + $this->createExtraUser($i); + $progress->advance(); + } + + $progress->finish("{$extraCount} demo user(s)"); + + return; + } + + for ($i = 1; $i <= $extraCount; $i++) { + $this->createExtraUser($i); + } + + $this->command->info(sprintf('Seeded %d extra demo user(s).', $extraCount)); + } + + private function createExtraUser(int $index): void + { + User::query()->create([ + 'name' => $this->displayNameForDemoAuthor(), + 'email' => sprintf('demo-user-%03d@%s', $index, self::DEMO_EMAIL_DOMAIN), + 'password' => Hash::make(self::DEMO_EXTRA_PASSWORD), + 'email_verified_at' => now(), + ]); + } + + /** + * User has no translations — names use de_DE Faker so author labels stay German in ?lang=de_DE. + */ + private function displayNameForDemoAuthor(): string + { + $faker = $this->fakerForLocale('de_DE'); + + return trim($faker->firstName().' '.$faker->lastName()); + } + + /** + * @return list + */ + private function defaultUserEmails(): array + { + return array_column(self::DEFAULT_USERS, 'email'); + } + + protected function seedDemoAssets(): void + { + if (! class_exists(ImportDemoMediaToMediathek::class)) { + return; + } + + if (! class_exists(Media::class)) { + return; + } + + $users = $this->seededDemoUsers(); + + $sourceDir = config('demo.media.users_path'); + + if (! is_string($sourceDir) || $sourceDir === '') { + return; + } + + $imagePaths = ImportDemoMediaToMediathek::listImagePaths($sourceDir, count($users)); + + if ($imagePaths === []) { + if ($this->command !== null) { + $this->command->warn(' No demo user images found in '.$sourceDir); + } + + return; + } + + $withAvatar = 0; + + foreach ($users as $index => $user) { + $imagePath = $imagePaths[$index] ?? null; + + if ($imagePath === null) { + continue; + } + + $media = ImportDemoMediaToMediathek::importFromPath($imagePath, null); + + if (! $media instanceof Media) { + continue; + } + + MediaUsable::query()->firstOrCreate([ + 'media_id' => $media->getKey(), + 'media_usable_id' => $user->getKey(), + 'media_usable_type' => User::class, + ]); + + $user->forceFill([ + 'avatar_url' => ImportDemoMediaToMediathek::avatarUrlFromMedia($media), + ])->saveQuietly(); + + $withAvatar++; + + if ($this->hasSeedOutput()) { + SeedOutput::created("Avatar for {$user->email}"); + } elseif ($this->command->getOutput()->isVerbose()) { + $this->command->line(" User {$user->email}: mediathek media #{$media->getKey()} ({$media->file_name})"); + } + } + + $this->reportDetail(sprintf('Attached avatars for %d user(s).', $withAvatar)); + } + + private function resolveExtraUserCount(): int + { + $smallDefault = (int) (config('demo.dataset_sizes.small') ?? 100); + + if (class_exists(SeedingConfig::class)) { + return SeedingConfig::resolveCount('user', $smallDefault); + } + + return $smallDefault; + } + + /** + * @return Collection + */ + private function seededDemoUsers(): Collection + { + return User::query() + ->whereIn('email', $this->defaultUserEmails()) + ->orWhere('email', 'like', 'demo-user-%@'.self::DEMO_EMAIL_DOMAIN) + ->orderBy('id') + ->get() + ->values(); + } + + private function purgeDemoUsers(): void + { + User::query() + ->whereIn('email', $this->defaultUserEmails()) + ->orWhere('email', 'like', 'demo-user-%@'.self::DEMO_EMAIL_DOMAIN) + ->forceDelete(); + } + + private function fakerForLocale(string $locale): Generator + { + static $cache = []; + $resolvedLocale = in_array($locale, $this->locales(), true) ? $locale : 'en_US'; + + if (! isset($cache[$resolvedLocale])) { + $cache[$resolvedLocale] = FakerFactory::create($resolvedLocale); + } + + return $cache[$resolvedLocale]; + } +}