feat: ship creator journey v2 and profile updates

This commit is contained in:
2026-04-12 21:42:07 +02:00
parent a2457f4e49
commit d5cff21ea2
335 changed files with 20147 additions and 1545 deletions

View File

@@ -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';