fix(gallery): fill tall portrait cards to full block width with object-cover crop
- ArtworkCard: add w-full to nova-card-media, use absolute inset-0 on img so object-cover fills the max-height capped box instead of collapsing the width - MasonryGallery.css: add width:100% to media container, position img absolutely so top/bottom is cropped rather than leaving dark gaps - Add React MasonryGallery + ArtworkCard components and entry point - Add recommendation system: UserRecoProfile model/DTO/migration, SuggestedCreatorsController, SuggestedTagsController, Recommendation services, config/recommendations.php - SimilarArtworksController, DiscoverController, HomepageService updates - Update routes (api + web) and discover/for-you views - Refresh favicon assets, update vite.config.js
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\Artwork;
|
||||
use App\Models\User;
|
||||
use App\Models\UserRecommendationCache;
|
||||
use App\Services\HomepageService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
config(['scout.driver' => 'null']);
|
||||
});
|
||||
|
||||
it('allForUser includes a for_you key', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
Cache::flush();
|
||||
|
||||
$service = app(HomepageService::class);
|
||||
$result = $service->allForUser($user);
|
||||
|
||||
expect($result)->toHaveKey('for_you')
|
||||
->and($result['for_you'])->toBeArray();
|
||||
});
|
||||
|
||||
it('allForUser for_you is an array even with no cached recommendations', function () {
|
||||
$user = User::factory()->create();
|
||||
$service = app(HomepageService::class);
|
||||
|
||||
Cache::flush();
|
||||
|
||||
$result = $service->allForUser($user);
|
||||
|
||||
expect($result['for_you'])->toBeArray();
|
||||
});
|
||||
|
||||
it('allForUser for_you returns items when cache exists', function () {
|
||||
$user = User::factory()->create();
|
||||
$artwork = Artwork::factory()->create([
|
||||
'is_public' => true,
|
||||
'is_approved' => true,
|
||||
'published_at' => now()->subHour(),
|
||||
]);
|
||||
|
||||
UserRecommendationCache::query()->create([
|
||||
'user_id' => $user->id,
|
||||
'algo_version' => (string) config('discovery.algo_version', 'clip-cosine-v1'),
|
||||
'cache_version' => (string) config('discovery.cache_version', 'cache-v1'),
|
||||
'recommendations_json' => [
|
||||
'items' => [
|
||||
['artwork_id' => $artwork->id, 'score' => 0.9, 'source' => 'profile'],
|
||||
],
|
||||
],
|
||||
'generated_at' => now(),
|
||||
'expires_at' => now()->addMinutes(60),
|
||||
]);
|
||||
|
||||
Cache::flush();
|
||||
|
||||
$service = app(HomepageService::class);
|
||||
$result = $service->allForUser($user);
|
||||
|
||||
expect($result['for_you'])->toBeArray();
|
||||
// At least one item should have the base shape (id, title, slug, url)
|
||||
if (count($result['for_you']) > 0) {
|
||||
expect($result['for_you'][0])->toHaveKeys(['id', 'title', 'slug', 'url']);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user