- 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
74 lines
2.5 KiB
PHP
74 lines
2.5 KiB
PHP
<?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();
|
|
}
|
|
}
|