Files
SkinbaseNova/app/Models/UserRecoProfile.php
Gregor Klevze 67ef79766c 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
2026-02-27 13:34:08 +01:00

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();
}
}