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:
73
app/Models/UserRecoProfile.php
Normal file
73
app/Models/UserRecoProfile.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\DTOs\UserRecoProfileDTO;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* Persisted cache of a user's recommendation preference profile.
|
||||
*
|
||||
* Schema: user_reco_profiles (user_id PK, json columns, timestamps).
|
||||
* Rebuilt by UserPreferenceBuilder → stale when updated_at < now() - TTL.
|
||||
*/
|
||||
class UserRecoProfile extends Model
|
||||
{
|
||||
protected $table = 'user_reco_profiles';
|
||||
|
||||
protected $primaryKey = 'user_id';
|
||||
public $incrementing = false;
|
||||
protected $keyType = 'int';
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
protected $casts = [
|
||||
'top_tags_json' => 'array',
|
||||
'top_categories_json' => 'array',
|
||||
'followed_creator_ids_json' => 'array',
|
||||
'tag_weights_json' => 'array',
|
||||
'category_weights_json' => 'array',
|
||||
'disliked_tag_ids_json' => 'array',
|
||||
];
|
||||
|
||||
// ── Relations ─────────────────────────────────────────────────────────────
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Hydrate a DTO from this model's JSON columns.
|
||||
*/
|
||||
public function toDTO(): UserRecoProfileDTO
|
||||
{
|
||||
return new UserRecoProfileDTO(
|
||||
topTagSlugs: (array) ($this->top_tags_json ?? []),
|
||||
topCategorySlugs: (array) ($this->top_categories_json ?? []),
|
||||
strongCreatorIds: array_map('intval', (array) ($this->followed_creator_ids_json ?? [])),
|
||||
tagWeights: array_map('floatval', (array) ($this->tag_weights_json ?? [])),
|
||||
categoryWeights: array_map('floatval', (array) ($this->category_weights_json ?? [])),
|
||||
dislikedTagSlugs: (array) ($this->disliked_tag_ids_json ?? []),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* True when the stored profile is still within the configured TTL.
|
||||
*/
|
||||
public function isFresh(): bool
|
||||
{
|
||||
if ($this->updated_at === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$ttl = (int) config('recommendations.ttl.user_reco_profile', 6 * 3600);
|
||||
|
||||
return $this->updated_at->addSeconds($ttl)->isFuture();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user