Skip to content

Commit

Permalink
feat(news): add a basic 'category' field (#3073)
Browse files Browse the repository at this point in the history
  • Loading branch information
wescopeland authored Jan 19, 2025
1 parent 671fec5 commit 7790651
Show file tree
Hide file tree
Showing 13 changed files with 205 additions and 10 deletions.
69 changes: 69 additions & 0 deletions app/Community/Enums/NewsCategory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

declare(strict_types=1);

namespace App\Community\Enums;

use Spatie\TypeScriptTransformer\Attributes\TypeScript;

#[TypeScript]
enum NewsCategory: string
{
/**
* Used for new achievement set releases and revisions.
* Examples:
* - "New set: Pokémon XD: Gale of Darkness"
*/
case AchievementSet = "achievement-set";

/**
* Used for community updates, spotlights, milestones, and weekly topics.
* Examples:
* - "Come Celebrate 1 MILLION Users!"
* - "Weekly Topic: Your First Achievement"
* - "Community Spotlight: Top Developers of 2024"
*/
case Community = "community";

/**
* Used for events, competitions, and special occasions.
* Examples:
* - "RetroAchievemas 2024 Event"
* - "Achievement of the Week 2025 Begins"
* - "Summer Games Done Quick Special Event"
*/
case Events = "events";

/**
* Used for achievement guides and tutorials.
* Examples:
* - "Guide Showcase: Harvest Moon DS: Cute"
* - "Achievement Guide: Final Fantasy VII"
* - "How to Get Started Making Achievements"
*/
case Guide = "guide";

/**
* Used for external media coverage and content featuring RetroAchievements.
* Examples:
* - "authorblues and Skybilz at AGDQ 2025"
* - "RetroAchievements Featured on IGN"
* - "Community Race: Mega Man X Any%"
* - "New YouTube Series: Achievement Hunting"
* - "RetroRGB Podcast Interview"
*/
case Media = "media";

/**
* Used by the engineering team for a "What's New" section on the home page.
*/
case SiteReleaseNotes = "site-release-notes";

/**
* Used for site maintenance, updates, and feature announcements.
* Examples:
* - "Upcoming Hardcore Restriction"
* - "Planned Site Maintenance"
*/
case Technical = "technical";
}
3 changes: 3 additions & 0 deletions app/Data/NewsData.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace App\Data;

use App\Community\Enums\NewsCategory;
use App\Models\News;
use Carbon\Carbon;
use Spatie\LaravelData\Data;
Expand All @@ -21,6 +22,7 @@ public function __construct(
public UserData $user,
public ?string $link,
public ?string $imageAssetPath,
public ?NewsCategory $category,
public ?Carbon $publishAt,
public ?Carbon $unpublishAt,
public ?Carbon $pinnedAt,
Expand All @@ -38,6 +40,7 @@ public static function fromNews(News $news): self
user: UserData::from($news->user),
link: $news->link,
imageAssetPath: $news->image_asset_path,
category: $news->category,
publishAt: $news->publish_at,
unpublishAt: $news->unpublish_at,
pinnedAt: $news->pinned_at,
Expand Down
30 changes: 29 additions & 1 deletion app/Filament/Resources/NewsResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace App\Filament\Resources;

use App\Community\Enums\NewsCategory;
use App\Filament\Extensions\Resources\Resource;
use App\Filament\Resources\NewsResource\Pages;
use App\Models\News;
Expand Down Expand Up @@ -51,10 +52,37 @@ public static function form(Form $form): Form
->required()
->activeUrl(),

Forms\Components\Select::make('category')
->label('Category')
->options([
NewsCategory::AchievementSet->value => 'Featured Set',
NewsCategory::Community->value => 'Community',
NewsCategory::Events->value => 'Events',
NewsCategory::Guide->value => 'Guide',
NewsCategory::Media->value => 'Media',
NewsCategory::Technical->value => 'Technical',
])
->helperText(function ($state) {
// Show an example based on selected category.
$example = match ($state) {
NewsCategory::AchievementSet->value => 'Example: "New set: Pokémon XD: Gale of Darkness"',
NewsCategory::Community->value => 'Example: "Come Celebrate 1 MILLION Users!"',
NewsCategory::Events->value => 'Example: "RetroAchievemas 2024 Event"',
NewsCategory::Guide->value => 'Example: "Achievement Guide: Final Fantasy VII"',
NewsCategory::Media->value => 'Example: "authorblues and Skybilz at AGDQ 2025"',
NewsCategory::Technical->value => 'Example: "Upcoming Hardcore Restriction"',
default => 'Optional.',
};

return $example;
})
->live()
->placeholder('No category')
->nullable(),

Forms\Components\Toggle::make('pinned_at')
->label('Pinned')
->helperText('If enabled, this will be sorted to the top of the news until unpinned.')
->columnSpanFull()
->disabled(fn (?News $record) => !$record || !$user->can('pin', $record))
->dehydrated()
->afterStateHydrated(function (?News $record, $component) {
Expand Down
3 changes: 3 additions & 0 deletions app/Models/News.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use App\Community\Concerns\HasAuthor;
use App\Community\Contracts\HasComments;
use App\Community\Enums\NewsCategory;
use App\Support\Database\Eloquent\BaseModel;
use Carbon\Carbon;
use Database\Factories\NewsFactory;
Expand Down Expand Up @@ -40,12 +41,14 @@ class News extends BaseModel implements HasComments, HasMedia
'user_id',
'link',
'image_asset_path',
'category',
'publish_at',
'unpublish_at',
'pinned_at',
];

protected $casts = [
'category' => NewsCategory::class,
'publish_at' => 'datetime',
'unpublish_at' => 'datetime',
'pinned_at' => 'datetime',
Expand Down
23 changes: 23 additions & 0 deletions database/migrations/2025_01_18_000000_update_news_table.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class() extends Migration {
public function up(): void
{
Schema::table('news', function (Blueprint $table) {
$table->string('category', 50)->nullable()->after('image_asset_path');
});
}

public function down(): void
{
Schema::table('news', function (Blueprint $table) {
$table->dropColumn('category');
});
}
};
6 changes: 6 additions & 0 deletions lang/en_US.json
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,12 @@
"Beaten by same players": "Beaten by same players",
"Mastered by same players": "Mastered by same players",
"<1>{{achievementTitle}}</1> from <2>{{gameTitle}}</2>": "<1>{{achievementTitle}}</1> from <2>{{gameTitle}}</2>",
"news-category.achievement-set": "Featured Set",
"news-category.community": "Community",
"news-category.events": "Events",
"news-category.guide": "Guide",
"news-category.media": "Media",
"news-category.technical": "Technical",
"Don't ask for links to copyrighted ROMs. Don't share links to copyrighted ROMs.": "Don't ask for links to copyrighted ROMs. Don't share links to copyrighted ROMs.",
"Start new topic": "Start new topic",
"enter your new topic's title...": "enter your new topic's title..."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -275,4 +275,19 @@ describe('Component: FrontPageNews', () => {
// ASSERT
expect(screen.queryByText('new')).not.toBeInTheDocument();
});

it('given a news post has a category, displays the category', () => {
// ARRANGE
const recentNews = [createNews({ title: 'Foo', category: 'achievement-set' })];

render<App.Http.Data.HomePageProps>(<FrontPageNews />, {
pageProps: {
recentNews,
ziggy: createZiggyProps({ device: 'desktop' }),
},
});

// ASSERT
expect(screen.getByText(/featured set/i)).toBeVisible();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@ import { usePageProps } from '@/common/hooks/usePageProps';
import { cn } from '@/common/utils/cn';
import { formatDate } from '@/common/utils/l10n/formatDate';

import { NewsCategoryLabel } from './NewsCategoryLabel';

interface NewsCardProps {
news: App.Data.News;

className?: string;
tagLabel?: string;
}

export const NewsCard: FC<NewsCardProps> = ({ news, className }) => {
Expand Down Expand Up @@ -54,14 +55,6 @@ export const NewsCard: FC<NewsCardProps> = ({ news, className }) => {
) : null}

<NewsCardImage src={news.imageAssetPath} />

{/* {tagLabel ? (
<div className="absolute bottom-2 right-2">
<div className="flex h-[22px] select-none items-center justify-center rounded-full bg-neutral-50 px-2 font-bold text-zinc-900">
{tagLabel}
</div>
</div>
) : null} */}
</div>

<div className="relative w-full">
Expand Down Expand Up @@ -96,6 +89,12 @@ export const NewsCard: FC<NewsCardProps> = ({ news, className }) => {
<span className="ml-1 normal-case italic">
{'·'} {t('by {{authorDisplayName}}', { authorDisplayName: news?.user.displayName })}
</span>

{news.category ? (
<>
{' · '} <NewsCategoryLabel category={news.category} />
</>
) : null}
</div>

<p className="mb-2 mt-2 text-balance text-base sm:mt-0 md:text-wrap">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { render, screen } from '@/test';

import { NewsCategoryLabel } from './NewsCategoryLabel';

describe('Component: NewsCategoryLabel', () => {
it('renders without crashing', () => {
// ARRANGE
const { container } = render(<NewsCategoryLabel category="events" />);

// ASSERT
expect(container).toBeTruthy();
});

it('displays the label', () => {
// ARRANGE
render(<NewsCategoryLabel category="events" />);

// ASSERT
expect(screen.getByText(/events/i)).toBeVisible();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { FC } from 'react';
import { useTranslation } from 'react-i18next';

interface NewsCategoryLabelProps {
category: App.Community.Enums.NewsCategory;
}

export const NewsCategoryLabel: FC<NewsCategoryLabelProps> = ({ category }) => {
const { t } = useTranslation();

return (
<span>
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any -- this is intentional */}
{t(`news-category.${category}` as any)}
</span>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './NewsCategoryLabel';
1 change: 1 addition & 0 deletions resources/js/test/factories/createNews.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const createNews = createFactory<App.Data.News>((faker) => {
createdAt: faker.date.recent().toISOString(),
id: faker.number.int({ min: 1, max: 10000 }),
imageAssetPath: faker.internet.url(),
category: null,
lead: faker.word.words(24),
link: faker.internet.url(),
pinnedAt: null,
Expand Down
9 changes: 9 additions & 0 deletions resources/js/types/generated.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,14 @@ declare namespace App.Community.Data {
}
declare namespace App.Community.Enums {
export type ArticleType = 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
export type NewsCategory =
| 'achievement-set'
| 'community'
| 'events'
| 'guide'
| 'media'
| 'site-release-notes'
| 'technical';
export type AwardType = 1 | 2 | 3 | 6 | 7 | 8 | 9;
export type ClaimSetType = 0 | 1;
export type ClaimStatus = 0 | 1 | 2 | 3;
Expand Down Expand Up @@ -216,6 +224,7 @@ declare namespace App.Data {
user: App.Data.User;
link: string | null;
imageAssetPath: string | null;
category: App.Community.Enums.NewsCategory | null;
publishAt: string | null;
unpublishAt: string | null;
pinnedAt: string | null;
Expand Down

0 comments on commit 7790651

Please sign in to comment.