Changed channel categories from DB-backed Category records to a backed PHP enum

This commit is contained in:
2026-05-18 06:55:03 +03:30
parent 295a408965
commit 59e4ae60da
20 changed files with 221 additions and 130 deletions

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Enums;
enum ChannelCategory: string
{
case Gaming = 'gaming';
case Music = 'music';
case TalkShows = 'talk-shows';
case Education = 'education';
case Creative = 'creative';
public function label(): string
{
return match ($this) {
self::Gaming => __('Gaming'),
self::Music => __('Music'),
self::TalkShows => __('Talk Shows'),
self::Education => __('Education'),
self::Creative => __('Creative'),
};
}
/**
* @return array{value: string, label: string}
*/
public function toOption(): array
{
return [
'value' => $this->value,
'label' => $this->label(),
];
}
/**
* @return list<array{value: string, label: string}>
*/
public static function options(): array
{
return array_map(
fn (self $category): array => $category->toOption(),
self::cases(),
);
}
}

View File

@@ -22,7 +22,7 @@ class ChannelController extends Controller
): Response {
abort_if($channel->isSuspended(), 404);
$channel->load(['category', 'liveBroadcast', 'user']);
$channel->load(['liveBroadcast', 'user']);
$channel->loadCount('follows');
$currentBroadcast = $channel->liveBroadcast;
@@ -39,11 +39,7 @@ class ChannelController extends Controller
'is_live' => $channel->is_live,
'viewer_count' => $channel->is_live ? $viewerCounts->current($channel) : 0,
'followers_count' => $channel->follows_count,
'category' => $channel->category ? [
'id' => $channel->category->id,
'name' => $channel->category->name,
'slug' => $channel->category->slug,
] : null,
'category' => $channel->category?->toOption(),
'owner' => [
'id' => $channel->user->id,
'name' => $channel->user->name,

View File

@@ -2,8 +2,8 @@
namespace App\Http\Controllers;
use App\Enums\ChannelCategory;
use App\Models\Broadcast;
use App\Models\Category;
use App\Models\Channel;
use App\Services\Streaming\StreamKeyManager;
use App\Services\Streaming\ViewerCountStore;
@@ -21,7 +21,7 @@ class CreatorDashboardController extends Controller
{
$channel = $request->user()
->channel()
->with(['category', 'liveBroadcast', 'streamKey'])
->with(['liveBroadcast', 'streamKey'])
->first();
return Inertia::render('dashboard', [
@@ -34,14 +34,7 @@ class CreatorDashboardController extends Controller
? $streamKeys->tokenizedPath($channel, (string) session('plain_stream_key'))
: null,
],
'categories' => Category::query()
->orderBy('name')
->get(['id', 'name', 'slug'])
->map(fn (Category $category) => [
'id' => $category->id,
'name' => $category->name,
'slug' => $category->slug,
]),
'categories' => ChannelCategory::options(),
'recentBroadcasts' => $channel
? $channel->broadcasts()->with('vod')->latest()->limit(10)->get()->map(fn (Broadcast $broadcast) => [
'id' => $broadcast->id,
@@ -65,7 +58,7 @@ class CreatorDashboardController extends Controller
'display_name' => ['required', 'string', 'max:80'],
'slug' => ['required', 'string', 'min:3', 'max:40', 'alpha_dash:ascii', 'lowercase', 'unique:channels,slug'],
'description' => ['nullable', 'string', 'max:1000'],
'category_id' => ['nullable', 'exists:categories,id'],
'category' => ['nullable', Rule::enum(ChannelCategory::class)],
]);
$channel = $request->user()->channel()->create([
@@ -92,7 +85,7 @@ class CreatorDashboardController extends Controller
'display_name' => ['required', 'string', 'max:80'],
'slug' => ['required', 'string', 'min:3', 'max:40', 'alpha_dash:ascii', 'lowercase', Rule::unique('channels', 'slug')->ignore($channel)],
'description' => ['nullable', 'string', 'max:1000'],
'category_id' => ['nullable', 'exists:categories,id'],
'category' => ['nullable', Rule::enum(ChannelCategory::class)],
'thumbnail' => ['nullable', 'image', 'mimes:jpg,jpeg,png,webp', 'max:4096'],
]);
@@ -187,7 +180,7 @@ class CreatorDashboardController extends Controller
'is_live' => $channel->is_live,
'viewer_count' => $channel->viewer_count,
'stream_key_last_used_at' => $channel->streamKey?->last_used_at?->toIso8601String(),
'category_id' => $channel->category_id,
'category' => $channel->category?->value,
'live_broadcast' => $channel->liveBroadcast ? [
'id' => $channel->liveBroadcast->id,
'title' => $channel->liveBroadcast->title,

View File

@@ -2,7 +2,7 @@
namespace App\Http\Controllers;
use App\Models\Category;
use App\Enums\ChannelCategory;
use App\Models\Channel;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
@@ -17,7 +17,7 @@ class HomeController extends Controller
$liveChannels = Channel::query()
->where('is_live', true)
->whereNull('suspended_at')
->with(['category', 'liveBroadcast', 'user'])
->with(['liveBroadcast', 'user'])
->withCount('follows')
->orderByDesc('viewer_count')
->latest()
@@ -28,14 +28,7 @@ class HomeController extends Controller
return Inertia::render('welcome', [
'canRegister' => Features::enabled(Features::registration()),
'liveChannels' => $liveChannels,
'categories' => Category::query()
->orderBy('name')
->get(['id', 'name', 'slug'])
->map(fn (Category $category) => [
'id' => $category->id,
'name' => $category->name,
'slug' => $category->slug,
]),
'categories' => ChannelCategory::options(),
]);
}
@@ -52,11 +45,7 @@ class HomeController extends Controller
'banner_url' => $this->mediaUrl($channel->banner_path),
'viewer_count' => $channel->viewer_count,
'followers_count' => $channel->follows_count ?? 0,
'category' => $channel->category ? [
'id' => $channel->category->id,
'name' => $channel->category->name,
'slug' => $channel->category->slug,
] : null,
'category' => $channel->category?->toOption(),
'broadcast' => $channel->liveBroadcast ? [
'id' => $channel->liveBroadcast->id,
'title' => $channel->liveBroadcast->title,

View File

@@ -1,21 +0,0 @@
<?php
namespace App\Models;
use Database\Factories\CategoryFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
#[Fillable(['name', 'slug', 'description'])]
class Category extends Model
{
/** @use HasFactory<CategoryFactory> */
use HasFactory;
public function channels(): HasMany
{
return $this->hasMany(Channel::class);
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Models;
use App\Enums\ChannelCategory;
use Database\Factories\ChannelFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -12,7 +13,7 @@ use Illuminate\Database\Eloquent\Relations\HasOne;
#[Fillable([
'user_id',
'category_id',
'category',
'live_broadcast_id',
'slug',
'display_name',
@@ -32,6 +33,7 @@ class Channel extends Model
protected function casts(): array
{
return [
'category' => ChannelCategory::class,
'is_live' => 'boolean',
'viewer_count' => 'integer',
'suspended_at' => 'datetime',
@@ -48,11 +50,6 @@ class Channel extends Model
return $this->belongsTo(User::class);
}
public function category(): BelongsTo
{
return $this->belongsTo(Category::class);
}
public function streamKey(): HasOne
{
return $this->hasOne(StreamKey::class);

View File

@@ -1,20 +0,0 @@
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
class CategoryFactory extends Factory
{
public function definition(): array
{
$name = fake()->unique()->words(2, true);
return [
'name' => Str::headline($name),
'slug' => Str::slug($name),
'description' => fake()->optional()->sentence(),
];
}
}

View File

@@ -2,7 +2,7 @@
namespace Database\Factories;
use App\Models\Category;
use App\Enums\ChannelCategory;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
@@ -15,7 +15,7 @@ class ChannelFactory extends Factory
return [
'user_id' => User::factory(),
'category_id' => Category::factory(),
'category' => fake()->randomElement(ChannelCategory::cases())->value,
'slug' => Str::slug($name),
'display_name' => Str::headline($name),
'description' => fake()->sentence(),

View File

@@ -0,0 +1,76 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('channels', function (Blueprint $table) {
$table->string('category')->nullable()->after('user_id')->index();
});
$allowedCategories = ['gaming', 'music', 'talk-shows', 'education', 'creative'];
DB::table('categories')
->select(['id', 'slug'])
->orderBy('id')
->get()
->each(function (object $category) use ($allowedCategories): void {
if (! in_array($category->slug, $allowedCategories, true)) {
return;
}
DB::table('channels')
->where('category_id', $category->id)
->update(['category' => $category->slug]);
});
Schema::table('channels', function (Blueprint $table) {
$table->dropConstrainedForeignId('category_id');
});
Schema::dropIfExists('categories');
}
public function down(): void
{
Schema::create('categories', function (Blueprint $table) {
$table->id();
$table->string('name')->unique();
$table->string('slug')->unique();
$table->text('description')->nullable();
$table->timestamps();
});
DB::table('categories')->insert([
['name' => 'Gaming', 'slug' => 'gaming', 'created_at' => now(), 'updated_at' => now()],
['name' => 'Music', 'slug' => 'music', 'created_at' => now(), 'updated_at' => now()],
['name' => 'Talk Shows', 'slug' => 'talk-shows', 'created_at' => now(), 'updated_at' => now()],
['name' => 'Education', 'slug' => 'education', 'created_at' => now(), 'updated_at' => now()],
['name' => 'Creative', 'slug' => 'creative', 'created_at' => now(), 'updated_at' => now()],
]);
Schema::table('channels', function (Blueprint $table) {
$table->foreignId('category_id')->nullable()->after('user_id')->constrained()->nullOnDelete();
});
DB::table('categories')
->select(['id', 'slug'])
->orderBy('id')
->get()
->each(function (object $category): void {
DB::table('channels')
->where('category', $category->slug)
->update(['category_id' => $category->id]);
});
Schema::table('channels', function (Blueprint $table) {
$table->dropIndex(['category']);
$table->dropColumn('category');
});
}
};

View File

@@ -2,7 +2,7 @@
namespace Database\Seeders;
use App\Models\Category;
use App\Enums\ChannelCategory;
use App\Models\PlatformSetting;
use App\Models\User;
use App\Services\Streaming\StreamKeyManager;
@@ -15,17 +15,6 @@ class DatabaseSeeder extends Seeder
*/
public function run(): void
{
collect([
['name' => 'Gaming', 'slug' => 'gaming'],
['name' => 'Music', 'slug' => 'music'],
['name' => 'Talk Shows', 'slug' => 'talk-shows'],
['name' => 'Education', 'slug' => 'education'],
['name' => 'Creative', 'slug' => 'creative'],
])->each(fn (array $category) => Category::query()->firstOrCreate(
['slug' => $category['slug']],
['name' => $category['name']],
));
PlatformSetting::current();
$admin = User::query()->firstOrNew(['email' => 'admin@nyone.net']);
@@ -47,7 +36,7 @@ class DatabaseSeeder extends Seeder
])->save();
$channel = $official->channel()->updateOrCreate([], [
'category_id' => Category::query()->where('slug', 'creative')->value('id'),
'category' => ChannelCategory::Creative,
'slug' => 'nyone',
'display_name' => 'Nyone Official',
'description' => 'Official updates and live sessions from Nyone.',

View File

@@ -82,6 +82,7 @@
"Create an account": "إنشاء حساب",
"Create channel": "إنشاء قناة",
"Create your channel": "أنشئ قناتك",
"Creative": "الإبداع",
"Creator Studio": "استوديو المنشئ",
"Creator access grants stay in the moderation console.": "تبقى أذونات وصول المنشئ في وحدة الإشراف.",
"Creator grants": "أذونات المنشئ",
@@ -101,6 +102,7 @@
"Don't have an account?": "ليس لديك حساب؟",
"Each account can own one public channel in this version.": "يمكن لكل حساب امتلاك قناة عامة واحدة في هذا الإصدار.",
"Each recovery code can be used once to access your account and will be removed after use. If you need more, click": "يمكن استخدام كل رمز استرداد مرة واحدة للوصول إلى حسابك وسيتم حذفه بعد الاستخدام. إذا احتجت إلى المزيد، فانقر",
"Education": "التعليم",
"Email": "البريد الإلكتروني",
"Email address": "عنوان البريد الإلكتروني",
"Email password reset link": "إرسال رابط إعادة تعيين كلمة المرور",
@@ -123,6 +125,7 @@
"Forgot password": "نسيت كلمة المرور",
"Forgot password?": "هل نسيت كلمة المرور؟",
"Full name": "الاسم الكامل",
"Gaming": "الألعاب",
"Grant": "منح",
"Hide password": "إخفاء كلمة المرور",
"Hide recovery codes": "إخفاء رموز الاسترداد",
@@ -152,6 +155,7 @@
"Message sent to admin.": "تم إرسال الرسالة إلى المسؤول.",
"Moderation console": "وحدة الإشراف",
"More": "المزيد",
"Music": "الموسيقى",
"NEEDS APPROVAL": "يتطلب موافقة",
"NEEDS CHANNEL GRANT": "يتطلب إذن قناة",
"Name": "الاسم",
@@ -268,6 +272,7 @@
"Suspend": "تعليق",
"Suspend channel?": "تعليق القناة؟",
"System": "النظام",
"Talk Shows": "برامج الحوار",
"The channel will be hidden from public browsing and watch pages.": "سيتم إخفاء القناة من التصفح العام وصفحات المشاهدة.",
"The channel will be visible again if the owning account is active.": "ستظهر القناة مرة أخرى إذا كان حساب المالك نشطًا.",
"The live floor is quiet": "ساحة البث هادئة",

View File

@@ -82,6 +82,7 @@
"Create an account": "Create an account",
"Create channel": "Create channel",
"Create your channel": "Create your channel",
"Creative": "Creative",
"Creator Studio": "Creator Studio",
"Creator access grants stay in the moderation console.": "Creator access grants stay in the moderation console.",
"Creator grants": "Creator grants",
@@ -101,6 +102,7 @@
"Don't have an account?": "Don't have an account?",
"Each account can own one public channel in this version.": "Each account can own one public channel in this version.",
"Each recovery code can be used once to access your account and will be removed after use. If you need more, click": "Each recovery code can be used once to access your account and will be removed after use. If you need more, click",
"Education": "Education",
"Email": "Email",
"Email address": "Email address",
"Email password reset link": "Email password reset link",
@@ -123,6 +125,7 @@
"Forgot password": "Forgot password",
"Forgot password?": "Forgot password?",
"Full name": "Full name",
"Gaming": "Gaming",
"Grant": "Grant",
"Hide password": "Hide password",
"Hide recovery codes": "Hide recovery codes",
@@ -152,6 +155,7 @@
"Message sent to admin.": "Message sent to admin.",
"Moderation console": "Moderation console",
"More": "More",
"Music": "Music",
"NEEDS APPROVAL": "NEEDS APPROVAL",
"NEEDS CHANNEL GRANT": "NEEDS CHANNEL GRANT",
"Name": "Name",
@@ -268,6 +272,7 @@
"Suspend": "Suspend",
"Suspend channel?": "Suspend channel?",
"System": "System",
"Talk Shows": "Talk Shows",
"The channel will be hidden from public browsing and watch pages.": "The channel will be hidden from public browsing and watch pages.",
"The channel will be visible again if the owning account is active.": "The channel will be visible again if the owning account is active.",
"The live floor is quiet": "The live floor is quiet",

View File

@@ -82,6 +82,7 @@
"Create an account": "Crear una cuenta",
"Create channel": "Crear canal",
"Create your channel": "Crea tu canal",
"Creative": "Creatividad",
"Creator Studio": "Estudio de creador",
"Creator access grants stay in the moderation console.": "Los permisos de acceso de creador permanecen en la consola de moderación.",
"Creator grants": "Permisos de creador",
@@ -101,6 +102,7 @@
"Don't have an account?": "¿No tienes una cuenta?",
"Each account can own one public channel in this version.": "Cada cuenta puede tener un canal público en esta versión.",
"Each recovery code can be used once to access your account and will be removed after use. If you need more, click": "Cada código de recuperación puede usarse una vez para acceder a tu cuenta y se eliminará después de usarlo. Si necesitas más, haz clic",
"Education": "Educación",
"Email": "Correo electrónico",
"Email address": "Dirección de correo electrónico",
"Email password reset link": "Enviar enlace para restablecer contraseña",
@@ -123,6 +125,7 @@
"Forgot password": "Olvidé mi contraseña",
"Forgot password?": "¿Olvidaste tu contraseña?",
"Full name": "Nombre completo",
"Gaming": "Videojuegos",
"Grant": "Conceder",
"Hide password": "Ocultar contraseña",
"Hide recovery codes": "Ocultar códigos de recuperación",
@@ -152,6 +155,7 @@
"Message sent to admin.": "Mensaje enviado al administrador.",
"Moderation console": "Consola de moderación",
"More": "Más",
"Music": "Música",
"NEEDS APPROVAL": "REQUIERE APROBACIÓN",
"NEEDS CHANNEL GRANT": "REQUIERE PERMISO DE CANAL",
"Name": "Nombre",
@@ -268,6 +272,7 @@
"Suspend": "Suspender",
"Suspend channel?": "¿Suspender canal?",
"System": "Sistema",
"Talk Shows": "Programas de conversación",
"The channel will be hidden from public browsing and watch pages.": "El canal se ocultará de la navegación pública y de las páginas de visualización.",
"The channel will be visible again if the owning account is active.": "El canal volverá a estar visible si la cuenta propietaria está activa.",
"The live floor is quiet": "No hay actividad en vivo",

View File

@@ -82,6 +82,7 @@
"Create an account": "ایجاد یک حساب",
"Create channel": "ایجاد کانال",
"Create your channel": "کانال خود را ایجاد کنید",
"Creative": "خلاقیت",
"Creator Studio": "استودیوی سازنده",
"Creator access grants stay in the moderation console.": "مجوزهای دسترسی سازنده در کنسول مدیریت باقی می‌مانند.",
"Creator grants": "مجوزهای سازنده",
@@ -101,6 +102,7 @@
"Don't have an account?": "حساب ندارید؟",
"Each account can own one public channel in this version.": "در این نسخه هر حساب می‌تواند یک کانال عمومی داشته باشد.",
"Each recovery code can be used once to access your account and will be removed after use. If you need more, click": "هر کد بازیابی فقط یک‌بار برای دسترسی به حساب شما قابل استفاده است و پس از استفاده حذف می‌شود. اگر به کدهای بیشتری نیاز دارید، کلیک کنید",
"Education": "آموزش",
"Email": "ایمیل",
"Email address": "آدرس ایمیل",
"Email password reset link": "ارسال لینک بازنشانی رمز عبور",
@@ -123,6 +125,7 @@
"Forgot password": "فراموشی رمز عبور",
"Forgot password?": "رمز عبور را فراموش کرده‌اید؟",
"Full name": "نام کامل",
"Gaming": "بازی",
"Grant": "اعطا",
"Hide password": "پنهان کردن رمز عبور",
"Hide recovery codes": "پنهان کردن کدهای بازیابی",
@@ -152,6 +155,7 @@
"Message sent to admin.": "پیام برای مدیر ارسال شد.",
"Moderation console": "کنسول مدیریت",
"More": "بیشتر",
"Music": "موسیقی",
"NEEDS APPROVAL": "نیازمند تأیید",
"NEEDS CHANNEL GRANT": "نیازمند مجوز کانال",
"Name": "نام",
@@ -268,6 +272,7 @@
"Suspend": "تعلیق",
"Suspend channel?": "کانال تعلیق شود؟",
"System": "سیستم",
"Talk Shows": "گفت‌وگو",
"The channel will be hidden from public browsing and watch pages.": "کانال از مرور عمومی و صفحات تماشا پنهان خواهد شد.",
"The channel will be visible again if the owning account is active.": "اگر حساب مالک فعال باشد، کانال دوباره قابل مشاهده خواهد شد.",
"The live floor is quiet": "فضای زنده آرام است",

View File

@@ -150,7 +150,7 @@ export default function ChannelShow({
<LiveState channel={channel} />
{channel.category && (
<span className="rounded-md border px-2 py-1 text-xs">
{channel.category.name}
{channel.category.label}
</span>
)}
</div>

View File

@@ -38,7 +38,7 @@ import {
update as updateChannelRoute,
} from '@/routes/creator/channel';
import { rotate as rotateStreamKeyRoute } from '@/routes/creator/stream-key';
import type { BroadcastSummary, Category } from '@/types';
import type { BroadcastSummary, ChannelCategory } from '@/types';
type CreatorChannel = {
id: number;
@@ -51,7 +51,7 @@ type CreatorChannel = {
is_live: boolean;
viewer_count: number;
stream_key_last_used_at: string | null;
category_id: number | null;
category: string | null;
live_broadcast: BroadcastSummary | null;
};
@@ -63,7 +63,7 @@ type Props = {
ingestServer: string;
tokenizedPath: string | null;
};
categories: Category[];
categories: ChannelCategory[];
recentBroadcasts: BroadcastSummary[];
};
@@ -90,14 +90,14 @@ export default function Dashboard({
display_name: '',
slug: '',
description: '',
category_id: '',
category: '',
});
const channelForm = useForm({
_method: 'PATCH',
display_name: channel?.display_name ?? '',
slug: channel?.slug ?? '',
description: channel?.description ?? '',
category_id: channel?.category_id ? String(channel.category_id) : '',
category: channel?.category ?? '',
thumbnail: null as File | null,
});
const broadcastForm = useForm({
@@ -235,13 +235,16 @@ export default function Dashboard({
placeholder={t('nyone-live')}
/>
</Field>
<Field label="Category">
<Field
label="Category"
error={createForm.errors.category}
>
<CategorySelect
categories={categories}
value={createForm.data.category_id}
value={createForm.data.category}
onChange={(value) =>
createForm.setData(
'category_id',
'category',
value,
)
}
@@ -440,13 +443,16 @@ export default function Dashboard({
}
/>
</Field>
<Field label="Category">
<Field
label="Category"
error={channelForm.errors.category}
>
<CategorySelect
categories={categories}
value={channelForm.data.category_id}
value={channelForm.data.category}
onChange={(value) =>
channelForm.setData(
'category_id',
'category',
value,
)
}
@@ -763,7 +769,7 @@ function CategorySelect({
value,
onChange,
}: {
categories: Category[];
categories: ChannelCategory[];
value: string;
onChange: (value: string) => void;
}) {
@@ -777,8 +783,8 @@ function CategorySelect({
>
<option value="">{t('No category')}</option>
{categories.map((category) => (
<option key={category.id} value={category.id}>
{category.name}
<option key={category.value} value={category.value}>
{category.label}
</option>
))}
</select>

View File

@@ -19,12 +19,12 @@ import { cn } from '@/lib/utils';
import { dashboard, home, login, register } from '@/routes';
import { show as showChannel } from '@/routes/channels';
import { index as supportIndex } from '@/routes/support';
import type { Category, ChannelCard } from '@/types';
import type { ChannelCard, ChannelCategory } from '@/types';
type Props = {
canRegister: boolean;
liveChannels: ChannelCard[];
categories: Category[];
categories: ChannelCategory[];
};
export default function Welcome({
@@ -43,7 +43,7 @@ export default function Welcome({
return liveChannels.filter((channel) => {
const matchesCategory =
category === 'all' || channel.category?.slug === category;
category === 'all' || channel.category?.value === category;
const matchesQuery =
normalizedQuery === '' ||
channel.display_name.toLowerCase().includes(normalizedQuery) ||
@@ -175,7 +175,7 @@ export default function Welcome({
</span>
<span className="min-w-0 break-words">
{featuredChannel.category
?.name ??
?.label ??
t('Uncategorized')}
</span>
<span className="inline-flex items-center gap-1">
@@ -292,8 +292,11 @@ export default function Welcome({
{t('All categories')}
</option>
{categories.map((item) => (
<option key={item.id} value={item.slug}>
{item.name}
<option
key={item.value}
value={item.value}
>
{item.label}
</option>
))}
</select>
@@ -356,7 +359,7 @@ function StreamCard({ channel }: { channel: ChannelCard }) {
</div>
<div className="flex items-center justify-between gap-3 text-sm text-muted-foreground">
<span className="truncate">
{channel.category?.name ?? t('Uncategorized')}
{channel.category?.label ?? t('Uncategorized')}
</span>
<span className="shrink-0">
{t(':count viewers', {

View File

@@ -1,7 +1,6 @@
export type Category = {
id: number;
name: string;
slug: string;
export type ChannelCategory = {
value: string;
label: string;
};
export type ChannelCard = {
@@ -15,7 +14,7 @@ export type ChannelCard = {
banner_url: string | null;
viewer_count: number;
followers_count: number;
category: Category | null;
category: ChannelCategory | null;
broadcast: {
id: number;
title: string;
@@ -38,7 +37,7 @@ export type ChannelDetail = {
is_live: boolean;
viewer_count: number;
followers_count: number;
category: Category | null;
category: ChannelCategory | null;
owner: {
id: number;
name: string;

View File

@@ -2,7 +2,7 @@
namespace Tests\Feature;
use App\Models\Category;
use App\Enums\ChannelCategory;
use App\Models\Channel;
use App\Models\PlatformSetting;
use App\Models\StreamKey;
@@ -17,13 +17,12 @@ class CreatorChannelTest extends TestCase
public function test_user_with_creator_access_can_create_one_channel_and_receives_one_time_stream_key(): void
{
$user = User::factory()->create(['can_create_channel' => true]);
$category = Category::factory()->create();
$response = $this->actingAs($user)->post(route('creator.channel.store'), [
'display_name' => 'Nyone Live',
'slug' => 'nyone-live',
'description' => 'Live builds and demos.',
'category_id' => $category->id,
'category' => ChannelCategory::Creative->value,
]);
$response
@@ -34,7 +33,7 @@ class CreatorChannelTest extends TestCase
'user_id' => $user->id,
'slug' => 'nyone-live',
'display_name' => 'Nyone Live',
'category_id' => $category->id,
'category' => ChannelCategory::Creative->value,
]);
$this->assertDatabaseCount(StreamKey::class, 1);

View File

@@ -2,6 +2,7 @@
namespace Tests\Feature;
use App\Enums\ChannelCategory;
use App\Models\Broadcast;
use App\Models\Channel;
use App\Models\User;
@@ -64,6 +65,25 @@ class DashboardTest extends TestCase
);
}
public function test_home_page_exposes_translated_channel_category_labels(): void
{
config(['app.locale' => 'es']);
Channel::factory()->create([
'is_live' => true,
'category' => ChannelCategory::Music,
]);
$this->get(route('home'))
->assertOk()
->assertInertia(fn (Assert $page) => $page
->component('welcome')
->where('liveChannels.0.category.value', ChannelCategory::Music->value)
->where('liveChannels.0.category.label', __('Music', [], 'es'))
->where('categories', fn ($categories) => $categories->firstWhere('value', ChannelCategory::Music->value)['label'] === __('Music', [], 'es')),
);
}
public function test_creator_dashboard_exposes_channel_display_media_props(): void
{
$user = User::factory()->create();
@@ -101,7 +121,7 @@ class DashboardTest extends TestCase
'display_name' => $channel->display_name,
'slug' => $channel->slug,
'description' => $channel->description,
'category_id' => $channel->category_id,
'category' => $channel->category?->value,
'thumbnail' => UploadedFile::fake()->image('thumbnail.jpg', 1280, 720),
])
->assertRedirect()
@@ -131,7 +151,7 @@ class DashboardTest extends TestCase
'display_name' => $channel->display_name,
'slug' => $channel->slug,
'description' => $channel->description,
'category_id' => $channel->category_id,
'category' => $channel->category?->value,
'thumbnail' => UploadedFile::fake()->create('thumbnail.txt', 8, 'text/plain'),
])
->assertRedirect(route('dashboard'))