This commit is contained in:
2026-03-20 21:17:26 +01:00
parent 1a62fcb81d
commit 29c3ff8572
229 changed files with 13147 additions and 2577 deletions

View 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\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use App\Models\UserAchievement;
class Achievement extends Model
{
use HasFactory;
protected $fillable = [
'name',
'slug',
'description',
'icon',
'xp_reward',
'type',
'condition_type',
'condition_value',
];
protected function casts(): array
{
return [
'xp_reward' => 'integer',
'condition_value' => 'integer',
];
}
public function userAchievements(): HasMany
{
return $this->hasMany(UserAchievement::class, 'achievement_id');
}
public function users(): BelongsToMany
{
return $this->belongsToMany(User::class, 'user_achievements', 'achievement_id', 'user_id')
->withPivot('unlocked_at');
}
}

View File

@@ -11,7 +11,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* Unified activity feed event.
*
* Types: upload | comment | favorite | award | follow
* target_type: artwork | user
* target_type: artwork | story | user
*
* @property int $id
* @property int $actor_id
@@ -54,6 +54,7 @@ class ActivityEvent extends Model
const TYPE_FOLLOW = 'follow';
const TARGET_ARTWORK = 'artwork';
const TARGET_STORY = 'story';
const TARGET_USER = 'user';
// ── Relations ─────────────────────────────────────────────────────────────

88
app/Models/Country.php Normal file
View File

@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
final class Country extends Model
{
use HasFactory;
protected $fillable = [
'iso',
'iso2',
'iso3',
'numeric_code',
'name',
'native',
'phone',
'continent',
'capital',
'currency',
'languages',
'name_common',
'name_official',
'region',
'subregion',
'flag_svg_url',
'flag_png_url',
'flag_emoji',
'active',
'sort_order',
'is_featured',
];
protected function casts(): array
{
return [
'active' => 'boolean',
'is_featured' => 'boolean',
'sort_order' => 'integer',
];
}
public function users(): HasMany
{
return $this->hasMany(User::class);
}
public function scopeActive(Builder $query): Builder
{
return $query->where('active', true);
}
public function scopeOrdered(Builder $query): Builder
{
return $query
->orderByDesc('is_featured')
->orderBy('sort_order')
->orderBy('name_common');
}
public function getFlagCssClassAttribute(): ?string
{
$iso2 = strtoupper((string) $this->iso2);
if (! preg_match('/^[A-Z]{2}$/', $iso2)) {
return null;
}
return 'fi fi-'.strtolower($iso2);
}
public function getLocalFlagPathAttribute(): ?string
{
$iso2 = strtoupper((string) $this->iso2);
if (! preg_match('/^[A-Z]{2}$/', $iso2)) {
return null;
}
return '/gfx/flags/shiny/24/'.rawurlencode($iso2).'.png';
}
}

View File

@@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class DashboardPreference extends Model
{
public const MAX_PINNED_SPACES = 8;
/**
* @var list<string>
*/
private const ALLOWED_PINNED_SPACES = [
'/dashboard/profile',
'/dashboard/notifications',
'/dashboard/comments/received',
'/dashboard/followers',
'/dashboard/following',
'/dashboard/favorites',
'/dashboard/artworks',
'/dashboard/gallery',
'/dashboard/awards',
'/creator/stories',
'/studio',
];
protected $table = 'dashboard_preferences';
protected $primaryKey = 'user_id';
public $incrementing = false;
protected $keyType = 'int';
protected $fillable = [
'user_id',
'pinned_spaces',
];
protected function casts(): array
{
return [
'pinned_spaces' => 'array',
];
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
}
/**
* @param array<int, mixed> $hrefs
* @return list<string>
*/
public static function sanitizePinnedSpaces(array $hrefs): array
{
$allowed = array_fill_keys(self::ALLOWED_PINNED_SPACES, true);
$sanitized = [];
foreach ($hrefs as $href) {
if (! is_string($href) || ! isset($allowed[$href])) {
continue;
}
if (in_array($href, $sanitized, true)) {
continue;
}
$sanitized[] = $href;
if (count($sanitized) >= self::MAX_PINNED_SPACES) {
break;
}
}
return $sanitized;
}
/**
* @return list<string>
*/
public static function pinnedSpacesForUser(User $user): array
{
$preference = static::query()->find($user->id);
$spaces = $preference?->pinned_spaces;
return is_array($spaces) ? static::sanitizePinnedSpaces($spaces) : [];
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Leaderboard extends Model
{
use HasFactory;
public const TYPE_CREATOR = 'creator';
public const TYPE_ARTWORK = 'artwork';
public const TYPE_STORY = 'story';
public const PERIOD_DAILY = 'daily';
public const PERIOD_WEEKLY = 'weekly';
public const PERIOD_MONTHLY = 'monthly';
public const PERIOD_ALL_TIME = 'all_time';
protected $fillable = [
'type',
'entity_id',
'score',
'period',
];
protected function casts(): array
{
return [
'entity_id' => 'integer',
'score' => 'float',
];
}
}

View File

@@ -5,6 +5,8 @@ declare(strict_types=1);
namespace App\Models;
use App\Models\StoryLike;
use App\Models\StoryBookmark;
use App\Models\StoryComment;
use App\Models\StoryView;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@ -101,6 +103,16 @@ class Story extends Model
return $this->hasMany(StoryLike::class, 'story_id');
}
public function comments(): HasMany
{
return $this->hasMany(StoryComment::class, 'story_id');
}
public function bookmarks(): HasMany
{
return $this->hasMany(StoryBookmark::class, 'story_id');
}
// ── Scopes ───────────────────────────────────────────────────────────
public function scopePublished($query)

View File

@@ -0,0 +1,36 @@
<?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 StoryBookmark extends Model
{
use HasFactory;
protected $fillable = [
'story_id',
'user_id',
];
protected $casts = [
'story_id' => 'integer',
'user_id' => 'integer',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
public function story(): BelongsTo
{
return $this->belongsTo(Story::class, 'story_id');
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
}
}

View File

@@ -0,0 +1,62 @@
<?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;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class StoryComment extends Model
{
use HasFactory;
use SoftDeletes;
protected $fillable = [
'story_id',
'user_id',
'parent_id',
'content',
'raw_content',
'rendered_content',
'is_approved',
];
protected $casts = [
'story_id' => 'integer',
'user_id' => 'integer',
'parent_id' => 'integer',
'is_approved' => 'boolean',
'created_at' => 'datetime',
'updated_at' => 'datetime',
'deleted_at' => 'datetime',
];
public function story(): BelongsTo
{
return $this->belongsTo(Story::class, 'story_id');
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
}
public function parent(): BelongsTo
{
return $this->belongsTo(self::class, 'parent_id');
}
public function replies(): HasMany
{
return $this->hasMany(self::class, 'parent_id')->orderBy('created_at');
}
public function approvedReplies(): HasMany
{
return $this->replies()->where('is_approved', true)->whereNull('deleted_at')->with(['user.profile', 'approvedReplies']);
}
}

View File

@@ -5,6 +5,7 @@ namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\HasMany;
use App\Models\SocialAccount;
@@ -12,6 +13,9 @@ use App\Models\Conversation;
use App\Models\ConversationParticipant;
use App\Models\Message;
use App\Models\Notification;
use App\Models\Achievement;
use App\Models\UserAchievement;
use App\Models\UserXpLog;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
@@ -50,6 +54,9 @@ class User extends Authenticatable
'spam_reports',
'approved_posts',
'flagged_posts',
'xp',
'level',
'rank',
'password',
'role',
'allow_messages_from',
@@ -88,6 +95,9 @@ class User extends Authenticatable
'spam_reports' => 'integer',
'approved_posts' => 'integer',
'flagged_posts' => 'integer',
'xp' => 'integer',
'level' => 'integer',
'rank' => 'string',
'password' => 'hashed',
'allow_messages_from' => 'string',
];
@@ -108,6 +118,16 @@ class User extends Authenticatable
return $this->hasOne(UserProfile::class, 'user_id');
}
public function dashboardPreference(): HasOne
{
return $this->hasOne(DashboardPreference::class, 'user_id');
}
public function country(): BelongsTo
{
return $this->belongsTo(Country::class);
}
public function statistics(): HasOne
{
return $this->hasOne(UserStatistic::class, 'user_id');
@@ -140,6 +160,22 @@ class User extends Authenticatable
return $this->hasMany(ProfileComment::class, 'profile_user_id');
}
public function xpLogs(): HasMany
{
return $this->hasMany(UserXpLog::class, 'user_id');
}
public function userAchievements(): HasMany
{
return $this->hasMany(UserAchievement::class, 'user_id');
}
public function achievements(): BelongsToMany
{
return $this->belongsToMany(Achievement::class, 'user_achievements', 'user_id', 'achievement_id')
->withPivot('unlocked_at');
}
// ── Messaging ────────────────────────────────────────────────────────────
public function conversations(): BelongsToMany

View File

@@ -0,0 +1,42 @@
<?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;
use App\Models\Achievement;
class UserAchievement extends Model
{
use HasFactory;
public $timestamps = false;
protected $fillable = [
'user_id',
'achievement_id',
'unlocked_at',
];
protected function casts(): array
{
return [
'user_id' => 'integer',
'achievement_id' => 'integer',
'unlocked_at' => 'datetime',
];
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function achievement(): BelongsTo
{
return $this->belongsTo(Achievement::class, 'achievement_id');
}
}

39
app/Models/UserXpLog.php Normal file
View File

@@ -0,0 +1,39 @@
<?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 UserXpLog extends Model
{
use HasFactory;
public $timestamps = false;
protected $fillable = [
'user_id',
'action',
'xp',
'reference_id',
'created_at',
];
protected function casts(): array
{
return [
'user_id' => 'integer',
'xp' => 'integer',
'reference_id' => 'integer',
'created_at' => 'datetime',
];
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}