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:
94
app/DTOs/UserRecoProfileDTO.php
Normal file
94
app/DTOs/UserRecoProfileDTO.php
Normal file
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\DTOs;
|
||||
|
||||
/**
|
||||
* Lightweight value object representing a user's recommendation preference profile.
|
||||
*
|
||||
* Built by UserPreferenceBuilder from signals:
|
||||
* - favourited artworks (+3)
|
||||
* - awards given (+5)
|
||||
* - creator follows (+2 for their tags)
|
||||
* - own uploads (category bias)
|
||||
*
|
||||
* Cached in `user_reco_profiles` with a configurable TTL (default 6 hours).
|
||||
*/
|
||||
final class UserRecoProfileDTO
|
||||
{
|
||||
/**
|
||||
* @param array<int, string> $topTagSlugs Top tag slugs by weighted score (up to 20)
|
||||
* @param array<int, string> $topCategorySlugs Top category slugs (up to 5)
|
||||
* @param array<int, int> $strongCreatorIds Followed creator user IDs (up to 50)
|
||||
* @param array<string, float> $tagWeights Tag slug → normalised weight (0–1)
|
||||
* @param array<string, float> $categoryWeights Category slug → normalised weight
|
||||
* @param array<int, string> $dislikedTagSlugs Future: blocked/hidden tag slugs
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly array $topTagSlugs = [],
|
||||
public readonly array $topCategorySlugs = [],
|
||||
public readonly array $strongCreatorIds = [],
|
||||
public readonly array $tagWeights = [],
|
||||
public readonly array $categoryWeights = [],
|
||||
public readonly array $dislikedTagSlugs = [],
|
||||
) {}
|
||||
|
||||
/**
|
||||
* True if the user has enough signals to drive personalised recommendations.
|
||||
*/
|
||||
public function hasSignals(): bool
|
||||
{
|
||||
return $this->topTagSlugs !== [] || $this->strongCreatorIds !== [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the normalised tag weight for a given slug (0.0 if unknown).
|
||||
*/
|
||||
public function tagWeight(string $slug): float
|
||||
{
|
||||
return (float) ($this->tagWeights[$slug] ?? 0.0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when the creator is in the user's strong-follow list.
|
||||
*/
|
||||
public function followsCreator(int $userId): bool
|
||||
{
|
||||
return in_array($userId, $this->strongCreatorIds, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialise for storage in the DB / Redis cache.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'top_tags' => $this->topTagSlugs,
|
||||
'top_categories' => $this->topCategorySlugs,
|
||||
'strong_creators' => $this->strongCreatorIds,
|
||||
'tag_weights' => $this->tagWeights,
|
||||
'category_weights' => $this->categoryWeights,
|
||||
'disliked_tags' => $this->dislikedTagSlugs,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-hydrate from a stored array (e.g. from the DB JSON column).
|
||||
*
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
return new self(
|
||||
topTagSlugs: (array) ($data['top_tags'] ?? []),
|
||||
topCategorySlugs: (array) ($data['top_categories'] ?? []),
|
||||
strongCreatorIds: array_map('intval', (array) ($data['strong_creators'] ?? [])),
|
||||
tagWeights: array_map('floatval', (array) ($data['tag_weights'] ?? [])),
|
||||
categoryWeights: array_map('floatval', (array) ($data['category_weights'] ?? [])),
|
||||
dislikedTagSlugs: (array) ($data['disliked_tags'] ?? []),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user