feat: ship creator journey v2 and profile updates
This commit is contained in:
@@ -52,6 +52,9 @@ class Artwork extends Model
|
||||
'hash',
|
||||
'file_ext',
|
||||
'thumb_ext',
|
||||
'has_missing_thumbnails',
|
||||
'missing_thumbnail_variants_json',
|
||||
'thumbnails_checked_at',
|
||||
'file_size',
|
||||
'mime_type',
|
||||
'width',
|
||||
@@ -60,6 +63,27 @@ class Artwork extends Model
|
||||
'visibility',
|
||||
'is_approved',
|
||||
'is_mature',
|
||||
'maturity_level',
|
||||
'maturity_source',
|
||||
'maturity_status',
|
||||
'maturity_ai_score',
|
||||
'maturity_ai_labels',
|
||||
'maturity_ai_label',
|
||||
'maturity_ai_confidence',
|
||||
'maturity_ai_model',
|
||||
'maturity_ai_threshold_used',
|
||||
'maturity_ai_analysis_time_ms',
|
||||
'maturity_ai_action_hint',
|
||||
'maturity_ai_advisory',
|
||||
'maturity_ai_status',
|
||||
'maturity_ai_detected_at',
|
||||
'maturity_declared_at',
|
||||
'maturity_flagged_at',
|
||||
'maturity_flag_reason',
|
||||
'maturity_reviewed_by',
|
||||
'maturity_reviewed_at',
|
||||
'maturity_reviewer_note',
|
||||
'maturity_mismatch_count',
|
||||
'published_at',
|
||||
'hash',
|
||||
'thumb_ext',
|
||||
@@ -90,7 +114,28 @@ class Artwork extends Model
|
||||
'visibility' => 'string',
|
||||
'is_approved' => 'boolean',
|
||||
'is_mature' => 'boolean',
|
||||
'maturity_level' => 'string',
|
||||
'maturity_source' => 'string',
|
||||
'maturity_status' => 'string',
|
||||
'maturity_ai_score' => 'float',
|
||||
'maturity_ai_labels' => 'array',
|
||||
'maturity_ai_label' => 'string',
|
||||
'maturity_ai_confidence' => 'float',
|
||||
'maturity_ai_model' => 'string',
|
||||
'maturity_ai_threshold_used' => 'float',
|
||||
'maturity_ai_analysis_time_ms' => 'integer',
|
||||
'maturity_ai_action_hint' => 'string',
|
||||
'maturity_ai_advisory' => 'string',
|
||||
'maturity_ai_status' => 'string',
|
||||
'maturity_ai_detected_at' => 'datetime',
|
||||
'maturity_declared_at' => 'datetime',
|
||||
'maturity_flagged_at' => 'datetime',
|
||||
'maturity_reviewed_at' => 'datetime',
|
||||
'maturity_mismatch_count' => 'integer',
|
||||
'has_missing_thumbnails' => 'boolean',
|
||||
'published_at' => 'datetime',
|
||||
'missing_thumbnail_variants_json' => 'array',
|
||||
'thumbnails_checked_at' => 'datetime',
|
||||
'published_as_type' => 'string',
|
||||
'published_as_id' => 'integer',
|
||||
'publish_at' => 'datetime',
|
||||
@@ -184,6 +229,11 @@ class Artwork extends Model
|
||||
return $this->belongsTo(Group::class);
|
||||
}
|
||||
|
||||
public function maturityAuditFinding(): HasOne
|
||||
{
|
||||
return $this->hasOne(ArtworkMaturityAuditFinding::class);
|
||||
}
|
||||
|
||||
public function uploadedBy(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'uploaded_by_user_id');
|
||||
@@ -297,12 +347,31 @@ class Artwork extends Model
|
||||
return $this->hasMany(ArtworkAward::class);
|
||||
}
|
||||
|
||||
public function medals(): HasMany
|
||||
{
|
||||
return $this->hasMany(ArtworkMedal::class, 'artwork_id');
|
||||
}
|
||||
|
||||
/** All file versions for this artwork (oldest first). */
|
||||
public function versions(): HasMany
|
||||
{
|
||||
return $this->hasMany(ArtworkVersion::class)->orderBy('version_number');
|
||||
}
|
||||
|
||||
public function outgoingEvolutionRelations(): HasMany
|
||||
{
|
||||
return $this->hasMany(ArtworkRelation::class, 'source_artwork_id')
|
||||
->orderBy('sort_order')
|
||||
->orderBy('id');
|
||||
}
|
||||
|
||||
public function incomingEvolutionRelations(): HasMany
|
||||
{
|
||||
return $this->hasMany(ArtworkRelation::class, 'target_artwork_id')
|
||||
->orderByDesc('updated_at')
|
||||
->orderByDesc('id');
|
||||
}
|
||||
|
||||
/** The currently active version record. */
|
||||
public function currentVersion(): BelongsTo
|
||||
{
|
||||
@@ -319,6 +388,11 @@ class Artwork extends Model
|
||||
return $this->hasOne(ArtworkAwardStat::class);
|
||||
}
|
||||
|
||||
public function medalStats(): HasOne
|
||||
{
|
||||
return $this->hasOne(ArtworkMedalStat::class, 'artwork_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the Meilisearch document for this artwork.
|
||||
* Includes all fields required for search, filtering, sorting, and display.
|
||||
@@ -385,12 +459,20 @@ class Artwork extends Model
|
||||
'published_at_ts' => $publishedSortAt?->getTimestamp() ?? 0,
|
||||
'is_public' => (bool) $this->is_public,
|
||||
'is_approved' => (bool) $this->is_approved,
|
||||
'is_mature' => (bool) $this->is_mature,
|
||||
'is_mature_effective' => (bool) ($this->is_mature || $this->maturity_level === 'mature' || $this->maturity_status === 'suspected'),
|
||||
'maturity_level' => (string) ($this->maturity_level ?? 'safe'),
|
||||
'maturity_status' => (string) ($this->maturity_status ?? 'clear'),
|
||||
'has_missing_thumbnails' => (bool) ($this->has_missing_thumbnails ?? false),
|
||||
'missing_thumbnail_rank' => (int) (($this->has_missing_thumbnails ?? false) ? 1 : 0),
|
||||
// ── Trending / discovery fields ────────────────────────────────────
|
||||
'trending_score_1h' => (float) ($this->trending_score_1h ?? 0),
|
||||
'trending_score_24h' => (float) ($this->trending_score_24h ?? 0),
|
||||
'trending_score_7d' => (float) ($this->trending_score_7d ?? 0),
|
||||
'favorites_count' => (int) ($stat?->favorites ?? 0),
|
||||
'awards_received_count' => (int) ($awardStat?->score_total ?? 0),
|
||||
'awards_score_7d' => (int) ($awardStat?->score_7d ?? 0),
|
||||
'awards_score_30d' => (int) ($awardStat?->score_30d ?? 0),
|
||||
'downloads_count' => (int) ($stat?->downloads ?? 0),
|
||||
// ── Ranking V2 fields ───────────────────────────────────────────────
|
||||
'ranking_score' => (float) ($stat?->ranking_score ?? 0),
|
||||
@@ -404,6 +486,8 @@ class Artwork extends Model
|
||||
'silver' => $awardStat?->silver_count ?? 0,
|
||||
'bronze' => $awardStat?->bronze_count ?? 0,
|
||||
'score' => $awardStat?->score_total ?? 0,
|
||||
'score_7d' => $awardStat?->score_7d ?? 0,
|
||||
'score_30d' => $awardStat?->score_30d ?? 0,
|
||||
],
|
||||
];
|
||||
}
|
||||
@@ -432,6 +516,32 @@ class Artwork extends Model
|
||||
->where("{$table}.published_at", '<=', now());
|
||||
}
|
||||
|
||||
public function scopeSafeForGeneralAudience(Builder $query): Builder
|
||||
{
|
||||
$table = $this->getTable();
|
||||
|
||||
return $query
|
||||
->whereRaw('COALESCE(' . $table . '.is_mature, 0) = 0')
|
||||
->whereRaw("COALESCE(" . $table . ".maturity_status, 'clear') != ?", ['suspected']);
|
||||
}
|
||||
|
||||
public function scopeWithoutMissingThumbnails(Builder $query): Builder
|
||||
{
|
||||
$table = $this->getTable();
|
||||
|
||||
return $query->where(function (Builder $thumbnailQuery) use ($table): void {
|
||||
$thumbnailQuery->whereNull("{$table}.has_missing_thumbnails")
|
||||
->orWhere("{$table}.has_missing_thumbnails", false);
|
||||
});
|
||||
}
|
||||
|
||||
public function scopeOrderMissingThumbnailsLast(Builder $query): Builder
|
||||
{
|
||||
$table = $this->getTable();
|
||||
|
||||
return $query->orderByRaw("CASE WHEN {$table}.has_missing_thumbnails = 1 THEN 1 ELSE 0 END ASC");
|
||||
}
|
||||
|
||||
public function getRouteKeyName(): string
|
||||
{
|
||||
return 'slug';
|
||||
|
||||
@@ -9,11 +9,12 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class ArtworkAward extends Model
|
||||
{
|
||||
protected $table = 'artwork_awards';
|
||||
protected $table = 'artwork_medals';
|
||||
|
||||
protected $fillable = [
|
||||
'artwork_id',
|
||||
'user_id',
|
||||
'medal_type',
|
||||
'medal',
|
||||
'weight',
|
||||
];
|
||||
@@ -27,11 +28,26 @@ class ArtworkAward extends Model
|
||||
public const MEDALS = ['gold', 'silver', 'bronze'];
|
||||
|
||||
public const WEIGHTS = [
|
||||
'gold' => 3,
|
||||
'silver' => 2,
|
||||
'gold' => 5,
|
||||
'silver' => 3,
|
||||
'bronze' => 1,
|
||||
];
|
||||
|
||||
public static function weightFor(string $medal): int
|
||||
{
|
||||
return (int) config('artwork_medals.weights.' . $medal, self::WEIGHTS[$medal] ?? 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, int>
|
||||
*/
|
||||
public static function weights(): array
|
||||
{
|
||||
return collect(self::MEDALS)
|
||||
->mapWithKeys(fn (string $medal): array => [$medal => self::weightFor($medal)])
|
||||
->all();
|
||||
}
|
||||
|
||||
public function artwork(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Artwork::class);
|
||||
@@ -41,4 +57,14 @@ class ArtworkAward extends Model
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function getMedalAttribute(): ?string
|
||||
{
|
||||
return $this->attributes['medal_type'] ?? null;
|
||||
}
|
||||
|
||||
public function setMedalAttribute(?string $value): void
|
||||
{
|
||||
$this->attributes['medal_type'] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,11 +9,11 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class ArtworkAwardStat extends Model
|
||||
{
|
||||
protected $table = 'artwork_award_stats';
|
||||
protected $table = 'artwork_medal_stats';
|
||||
|
||||
public $primaryKey = 'artwork_id';
|
||||
public $incrementing = false;
|
||||
public $timestamps = false;
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'artwork_id',
|
||||
@@ -21,6 +21,10 @@ class ArtworkAwardStat extends Model
|
||||
'silver_count',
|
||||
'bronze_count',
|
||||
'score_total',
|
||||
'score_7d',
|
||||
'score_30d',
|
||||
'last_medaled_at',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
|
||||
@@ -30,6 +34,10 @@ class ArtworkAwardStat extends Model
|
||||
'silver_count' => 'integer',
|
||||
'bronze_count' => 'integer',
|
||||
'score_total' => 'integer',
|
||||
'score_7d' => 'integer',
|
||||
'score_30d' => 'integer',
|
||||
'last_medaled_at' => 'datetime',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
];
|
||||
|
||||
|
||||
@@ -3,21 +3,33 @@ namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class ArtworkFeature extends Model
|
||||
{
|
||||
protected $table = 'artwork_features';
|
||||
use SoftDeletes;
|
||||
|
||||
public $timestamps = false;
|
||||
protected $table = 'artwork_features';
|
||||
|
||||
protected $fillable = [
|
||||
'artwork_id',
|
||||
'type',
|
||||
'featured_at',
|
||||
'expires_at',
|
||||
'priority',
|
||||
'label',
|
||||
'note',
|
||||
'is_active',
|
||||
'force_hero',
|
||||
'created_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'featured_at' => 'datetime',
|
||||
'expires_at' => 'datetime',
|
||||
'priority' => 'integer',
|
||||
'is_active' => 'boolean',
|
||||
'force_hero' => 'boolean',
|
||||
];
|
||||
|
||||
public function artwork(): BelongsTo
|
||||
|
||||
61
app/Models/ArtworkMaturityAuditFinding.php
Normal file
61
app/Models/ArtworkMaturityAuditFinding.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
final class ArtworkMaturityAuditFinding extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public const STATUS_OPEN = 'open';
|
||||
public const STATUS_REVIEWED = 'reviewed';
|
||||
public const STATUS_CLEARED = 'cleared';
|
||||
|
||||
protected $fillable = [
|
||||
'artwork_id',
|
||||
'status',
|
||||
'thumbnail_variant',
|
||||
'ai_label',
|
||||
'ai_confidence',
|
||||
'ai_score',
|
||||
'ai_labels',
|
||||
'ai_model',
|
||||
'ai_threshold_used',
|
||||
'ai_analysis_time_ms',
|
||||
'ai_action_hint',
|
||||
'ai_status',
|
||||
'ai_advisory',
|
||||
'detected_at',
|
||||
'last_scanned_at',
|
||||
'resolution_action',
|
||||
'resolution_note',
|
||||
'resolved_by',
|
||||
'resolved_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'ai_confidence' => 'float',
|
||||
'ai_score' => 'float',
|
||||
'ai_labels' => 'array',
|
||||
'ai_threshold_used' => 'float',
|
||||
'ai_analysis_time_ms' => 'integer',
|
||||
'detected_at' => 'datetime',
|
||||
'last_scanned_at' => 'datetime',
|
||||
'resolved_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function artwork(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Artwork::class);
|
||||
}
|
||||
|
||||
public function resolver(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'resolved_by');
|
||||
}
|
||||
}
|
||||
9
app/Models/ArtworkMedal.php
Normal file
9
app/Models/ArtworkMedal.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
class ArtworkMedal extends ArtworkAward
|
||||
{
|
||||
}
|
||||
9
app/Models/ArtworkMedalStat.php
Normal file
9
app/Models/ArtworkMedalStat.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
class ArtworkMedalStat extends ArtworkAwardStat
|
||||
{
|
||||
}
|
||||
48
app/Models/ArtworkRelation.php
Normal file
48
app/Models/ArtworkRelation.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
final class ArtworkRelation extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public const TYPE_REMAKE_OF = 'remake_of';
|
||||
public const TYPE_REMASTER_OF = 'remaster_of';
|
||||
public const TYPE_REVISION_OF = 'revision_of';
|
||||
public const TYPE_INSPIRED_BY = 'inspired_by';
|
||||
public const TYPE_VARIATION_OF = 'variation_of';
|
||||
|
||||
protected $fillable = [
|
||||
'source_artwork_id',
|
||||
'target_artwork_id',
|
||||
'relation_type',
|
||||
'note',
|
||||
'sort_order',
|
||||
'created_by_user_id',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'sort_order' => 'integer',
|
||||
];
|
||||
|
||||
public function sourceArtwork(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Artwork::class, 'source_artwork_id');
|
||||
}
|
||||
|
||||
public function targetArtwork(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Artwork::class, 'target_artwork_id');
|
||||
}
|
||||
|
||||
public function creator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by_user_id');
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,27 @@ class Category extends Model
|
||||
|
||||
protected $casts = ['is_active' => 'boolean'];
|
||||
|
||||
public function getNameAttribute(?string $value): ?string
|
||||
{
|
||||
return self::decodeHtmlEntities($value);
|
||||
}
|
||||
|
||||
public function setNameAttribute(?string $value): void
|
||||
{
|
||||
$normalized = self::decodeHtmlEntities($value);
|
||||
$this->attributes['name'] = $normalized !== null ? trim($normalized) : null;
|
||||
}
|
||||
|
||||
public function getDescriptionAttribute(?string $value): ?string
|
||||
{
|
||||
return self::decodeHtmlEntities($value);
|
||||
}
|
||||
|
||||
public function setDescriptionAttribute(?string $value): void
|
||||
{
|
||||
$this->attributes['description'] = self::decodeHtmlEntities($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure slug is always lowercase and valid before saving.
|
||||
*/
|
||||
@@ -159,4 +180,25 @@ class Category extends Model
|
||||
|
||||
return $category;
|
||||
}
|
||||
|
||||
private static function decodeHtmlEntities(?string $value): ?string
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$decoded = $value;
|
||||
|
||||
for ($index = 0; $index < 5; $index++) {
|
||||
$next = html_entity_decode($decoded, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
|
||||
if ($next === $decoded) {
|
||||
break;
|
||||
}
|
||||
|
||||
$decoded = $next;
|
||||
}
|
||||
|
||||
return $decoded;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -653,19 +653,26 @@ class Collection extends Model
|
||||
return $this->isPubliclyAccessible();
|
||||
}
|
||||
|
||||
public function resolvedCoverArtwork(bool $publicOnly = false): ?Artwork
|
||||
public function resolvedCoverArtwork(bool $publicOnly = false, bool $hideMature = false): ?Artwork
|
||||
{
|
||||
$cover = $this->relationLoaded('coverArtwork') ? $this->coverArtwork : $this->coverArtwork()->first();
|
||||
if ($cover && (! $publicOnly || $this->artworkIsPubliclyVisible($cover))) {
|
||||
if ($cover && $this->artworkMatchesCoverVisibility($cover, $publicOnly, $hideMature)) {
|
||||
return $cover;
|
||||
}
|
||||
|
||||
$relation = $publicOnly ? 'publicArtworks' : 'artworks';
|
||||
$artworks = $this->relationLoaded($relation)
|
||||
? $this->getRelation($relation)
|
||||
: $this->{$relation}()->limit(1)->get();
|
||||
: $this->{$relation}()
|
||||
->when($hideMature, function ($query): void {
|
||||
$query->whereRaw('COALESCE(artworks.is_mature, 0) = 0')
|
||||
->whereRaw("COALESCE(artworks.maturity_status, 'clear') != ?", ['suspected']);
|
||||
})
|
||||
->limit(1)
|
||||
->get();
|
||||
|
||||
return $artworks->first();
|
||||
return $artworks
|
||||
->first(fn (Artwork $artwork): bool => $this->artworkMatchesCoverVisibility($artwork, $publicOnly, $hideMature));
|
||||
}
|
||||
|
||||
public function syncArtworksCount(): void
|
||||
@@ -712,4 +719,18 @@ class Collection extends Model
|
||||
&& $artwork->published_at !== null
|
||||
&& $artwork->published_at->lte(now());
|
||||
}
|
||||
|
||||
private function artworkMatchesCoverVisibility(Artwork $artwork, bool $publicOnly, bool $hideMature): bool
|
||||
{
|
||||
if ($publicOnly && ! $this->artworkIsPubliclyVisible($artwork)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! $hideMature) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return ! (bool) $artwork->is_mature
|
||||
&& (string) ($artwork->maturity_status ?? 'clear') !== 'suspected';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,10 +10,11 @@ use App\Models\Artwork;
|
||||
|
||||
class ContentType extends Model
|
||||
{
|
||||
protected $fillable = ['name','slug','description','order'];
|
||||
protected $fillable = ['name','slug','description','order','hide_from_menu','mascot_path','cover_art_path'];
|
||||
|
||||
protected $casts = [
|
||||
'order' => 'integer',
|
||||
'hide_from_menu' => 'boolean',
|
||||
];
|
||||
|
||||
public function scopeOrdered(EloquentBuilder $query): EloquentBuilder
|
||||
@@ -21,6 +22,11 @@ class ContentType extends Model
|
||||
return $query->orderBy('order')->orderBy('name')->orderBy('id');
|
||||
}
|
||||
|
||||
public function scopeVisibleInToolbar(EloquentBuilder $query): EloquentBuilder
|
||||
{
|
||||
return $query->where('hide_from_menu', false);
|
||||
}
|
||||
|
||||
public function categories(): HasMany
|
||||
{
|
||||
return $this->hasMany(Category::class);
|
||||
@@ -31,6 +37,11 @@ class ContentType extends Model
|
||||
return $this->categories()->whereNull('parent_id');
|
||||
}
|
||||
|
||||
public function slugHistories(): HasMany
|
||||
{
|
||||
return $this->hasMany(ContentTypeSlugHistory::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an Eloquent builder for Artworks that belong to this content type.
|
||||
* This traverses the pivot `artwork_category` via the `categories` relation.
|
||||
@@ -43,8 +54,33 @@ class ContentType extends Model
|
||||
});
|
||||
}
|
||||
|
||||
public function getMascotUrlAttribute(): ?string
|
||||
{
|
||||
return $this->resolveAssetUrl($this->mascot_path);
|
||||
}
|
||||
|
||||
public function getCoverArtUrlAttribute(): ?string
|
||||
{
|
||||
return $this->resolveAssetUrl($this->cover_art_path);
|
||||
}
|
||||
|
||||
public function getRouteKeyName(): string
|
||||
{
|
||||
return 'slug';
|
||||
}
|
||||
|
||||
private function resolveAssetUrl(?string $path): ?string
|
||||
{
|
||||
$path = trim((string) $path);
|
||||
|
||||
if ($path === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (str_starts_with($path, 'http://') || str_starts_with($path, 'https://') || str_starts_with($path, '/')) {
|
||||
return $path;
|
||||
}
|
||||
|
||||
return rtrim((string) config('cdn.files_url', 'https://cdn.skinbase.org'), '/') . '/' . ltrim($path, '/');
|
||||
}
|
||||
}
|
||||
|
||||
19
app/Models/ContentTypeSlugHistory.php
Normal file
19
app/Models/ContentTypeSlugHistory.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class ContentTypeSlugHistory extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'content_type_id',
|
||||
'old_slug',
|
||||
];
|
||||
|
||||
public function contentType(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ContentType::class);
|
||||
}
|
||||
}
|
||||
45
app/Models/CreatorEra.php
Normal file
45
app/Models/CreatorEra.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property int $user_id
|
||||
* @property string $era_type
|
||||
* @property string $title
|
||||
* @property string|null $description
|
||||
* @property \Carbon\Carbon $starts_at
|
||||
* @property \Carbon\Carbon|null $ends_at
|
||||
* @property bool $is_current
|
||||
* @property array|null $metadata
|
||||
*/
|
||||
final class CreatorEra extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'era_type',
|
||||
'title',
|
||||
'description',
|
||||
'starts_at',
|
||||
'ends_at',
|
||||
'is_current',
|
||||
'metadata',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'starts_at' => 'datetime',
|
||||
'ends_at' => 'datetime',
|
||||
'is_current' => 'boolean',
|
||||
'metadata' => 'array',
|
||||
];
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
46
app/Models/CreatorMilestone.php
Normal file
46
app/Models/CreatorMilestone.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class CreatorMilestone extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'type',
|
||||
'occurred_at',
|
||||
'occurred_year',
|
||||
'related_artwork_id',
|
||||
'is_public',
|
||||
'priority',
|
||||
'payload_json',
|
||||
'computed_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'occurred_at' => 'datetime',
|
||||
'occurred_year' => 'integer',
|
||||
'related_artwork_id' => 'integer',
|
||||
'is_public' => 'boolean',
|
||||
'priority' => 'integer',
|
||||
'payload_json' => 'array',
|
||||
'computed_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function artwork(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Artwork::class, 'related_artwork_id');
|
||||
}
|
||||
}
|
||||
@@ -124,6 +124,11 @@ class User extends Authenticatable
|
||||
return $this->hasMany(Artwork::class);
|
||||
}
|
||||
|
||||
public function givenArtworkMedals(): HasMany
|
||||
{
|
||||
return $this->hasMany(ArtworkMedal::class, 'user_id');
|
||||
}
|
||||
|
||||
public function collections(): HasMany
|
||||
{
|
||||
return $this->hasMany(Collection::class)->latest('updated_at');
|
||||
|
||||
@@ -35,6 +35,8 @@ class UserProfile extends Model
|
||||
'follower_notifications',
|
||||
'comment_notifications',
|
||||
'newsletter',
|
||||
'mature_content_visibility',
|
||||
'mature_content_warning_enabled',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
@@ -46,6 +48,7 @@ class UserProfile extends Model
|
||||
'follower_notifications' => 'boolean',
|
||||
'comment_notifications' => 'boolean',
|
||||
'newsletter' => 'boolean',
|
||||
'mature_content_warning_enabled' => 'boolean',
|
||||
];
|
||||
|
||||
public $timestamps = true;
|
||||
|
||||
Reference in New Issue
Block a user