Skip to content

Commit

Permalink
add achievement checklist page (#3018)
Browse files Browse the repository at this point in the history
  • Loading branch information
Jamiras authored Jan 9, 2025
1 parent 5109a31 commit face8a7
Show file tree
Hide file tree
Showing 19 changed files with 799 additions and 2 deletions.
89 changes: 89 additions & 0 deletions app/Community/Actions/BuildAchievementChecklistAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php

declare(strict_types=1);

namespace App\Community\Actions;

use App\Community\Data\AchievementGroupData;
use App\Models\Achievement;
use App\Models\PlayerAchievement;
use App\Models\User;
use App\Platform\Data\AchievementData;

class BuildAchievementChecklistAction
{
public function execute(
string $encoded,
User $user,
): array {
$groups = [];
foreach (explode('|', $encoded) as $group) {
if (!empty($group)) {
$groups[] = $this->parseGroup($group);
}
}

return $this->fillData($groups, $user);
}

private function parseGroup(string $group): array
{
$index = strrpos($group, ':');
if ($index === false) {
$header = '';
$ids = $group;
} else {
$header = substr($group, 0, $index);
$ids = substr($group, $index + 1);
}

$achievementIds = [];
foreach (explode(',', $ids) as $id) {
$achievementIds[] = (int) $id;
}

return [
'header' => $header,
'achievementIds' => $achievementIds,
];
}

/**
* @return AchievementGroupData[]
*/
private function fillData(array $groups, User $user): array
{
$ids = [];
foreach ($groups as $group) {
$ids = array_merge($ids, $group['achievementIds']);
}
$ids = array_unique($ids);

$achievements = Achievement::whereIn('ID', $ids)->with('game')->get();
$unlocks = PlayerAchievement::where('user_id', $user->id)->whereIn('achievement_id', $ids)->get();

$result = [];
foreach ($groups as $group) {
$achievementList = [];
foreach ($group['achievementIds'] as $achievementId) {
$achievement = $achievements->filter(fn ($a) => $a->ID === $achievementId)->first();
if ($achievement) {
$unlock = $unlocks->filter(fn ($u) => $u->achievement_id === $achievementId)->first();
$achievementList[] = AchievementData::from($achievement, $unlock)->include(
'description',
'points',
'badgeUnlockedUrl',
'badgeLockedUrl',
'unlockedAt',
'unlockedHardcoreAt',
'game.badgeUrl',
);
}
}

$result[] = new AchievementGroupData($group['header'], $achievementList);
}

return $result;
}
}
33 changes: 33 additions & 0 deletions app/Community/Controllers/UserAchievementChecklistController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace App\Community\Controllers;

use App\Community\Actions\BuildAchievementChecklistAction;
use App\Community\Data\AchievementChecklistPagePropsData;
use App\Data\UserData;
use App\Http\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response as InertiaResponse;

class UserAchievementChecklistController extends Controller
{
public function index(Request $request, User $user): InertiaResponse
{
$this->authorize('view', $user);

$list = $request->get('list');

$groups = (new BuildAchievementChecklistAction())->execute($list, $user);

$props = new AchievementChecklistPagePropsData(
UserData::fromUser($user),
$groups,
);

return Inertia::render('user/[user]/achievement-checklist', $props);
}
}
21 changes: 21 additions & 0 deletions app/Community/Data/AchievementChecklistPagePropsData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace App\Community\Data;

use App\Data\UserData;
use Spatie\LaravelData\Data;
use Spatie\TypeScriptTransformer\Attributes\TypeScript;

#[TypeScript('AchievementChecklistPageProps')]
class AchievementChecklistPagePropsData extends Data
{
public function __construct(
public UserData $player,

/** @var AchievementGroupData[] */
public array $groups,
) {
}
}
21 changes: 21 additions & 0 deletions app/Community/Data/AchievementGroupData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace App\Community\Data;

use App\Platform\Data\AchievementData;
use Spatie\LaravelData\Data;
use Spatie\TypeScriptTransformer\Attributes\TypeScript;

#[TypeScript('AchievementGroup')]
class AchievementGroupData extends Data
{
public function __construct(
public string $header,

/** @var AchievementData[] */
public array $achievements,
) {
}
}
2 changes: 2 additions & 0 deletions app/Community/RouteServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
use App\Community\Controllers\LeaderboardCommentController;
use App\Community\Controllers\MessageController;
use App\Community\Controllers\MessageThreadController;
use App\Community\Controllers\UserAchievementChecklistController;
use App\Community\Controllers\UserCommentController;
use App\Community\Controllers\UserForumTopicCommentController;
use App\Community\Controllers\UserGameListController;
Expand Down Expand Up @@ -115,6 +116,7 @@ protected function mapWebRoutes(): void
Route::get('forums/recent-posts', [ForumTopicController::class, 'recentPosts'])->name('forum.recent-posts');

Route::get('user/{user}/posts', [UserForumTopicCommentController::class, 'index'])->name('user.posts.index');
Route::get('user/{user}/achievement-checklist', [UserAchievementChecklistController::class, 'index'])->name('user.achievement-checklist');

Route::get('settings', [UserSettingsController::class, 'show'])->name('settings.show');
});
Expand Down
4 changes: 4 additions & 0 deletions lang/en_US.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"Accountability for content": "Accountability for content",
"Accountability for links": "Accountability for links",
"Achievement": "Achievement",
"Achievement Checklist": "Achievement Checklist",
"Achievement Unlocks": "Achievement Unlocks",
"Achievement of the Week": "Achievement of the Week",
"Achievements": "Achievements",
Expand Down Expand Up @@ -135,6 +136,7 @@
"Forum Index": "Forum Index",
"Forum Posts": "Forum Posts",
"Forum Posts - {{user}}": "Forum Posts - {{user}}",
"from": "from",
"Game": "Game",
"Game Details": "Game Details",
"Games": "Games",
Expand Down Expand Up @@ -165,6 +167,7 @@
"Important": "Important",
"Including your correct emulator version helps developers more quickly identify and resolve issues.": "Including your correct emulator version helps developers more quickly identify and resolve issues.",
"Information about cookies": "Information about cookies",
"Invalid list": "Invalid list",
"Issue": "Issue",
"Join us on Discord": "Join us on Discord",
"Just Released": "Just Released",
Expand Down Expand Up @@ -352,6 +355,7 @@
"Type / to focus the search field.": "Type / to focus the search field.",
"Type your comment here. Do not post or request any links to copyrighted ROMs.": "Type your comment here. Do not post or request any links to copyrighted ROMs.",
"Undo": "Undo",
"Unlocked {{when}}": "Unlocked {{when}}",
"Unpublished": "Unpublished",
"Unsubscribe": "Unsubscribe",
"Unsubscribed!": "Unsubscribed!",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { render, screen } from '@/test';
import { createAchievement, createAchievementGroup, createGame } from '@/test/factories';

import { AchievementGroup } from './AchievementGroup';

describe('Component: AchievementGroup', () => {
it('renders without crashing', () => {
// ARRANGE
const group = createAchievementGroup();
const { container } = render(<AchievementGroup group={group} />);

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

it('displays the header', () => {
// ARRANGE
const group = createAchievementGroup({
header: 'Creative Name',
});

render(<AchievementGroup group={group} />);

// ASSERT
expect(screen.getByText(/Creative Name/)).toBeVisible();
});

it('displays the achievements without games', () => {
// ARRANGE
const group = createAchievementGroup({
achievements: [
createAchievement({
title: 'First Achievement',
description: 'Do the first thing',
game: createGame({ title: 'First Game' }),
}),
createAchievement({ title: 'Second Achievement', description: 'Do the second thing' }),
createAchievement({ title: 'Third Achievement', description: 'Do the third thing' }),
],
});

render(<AchievementGroup group={group} />);

// ASSERT
expect(screen.getByText(/First Achievement/)).toBeVisible();
expect(screen.queryByText(/First Game/)).toBeNull();
expect(screen.getByText(/Do the first thing/)).toBeVisible();
expect(screen.getByText(/Second Achievement/)).toBeVisible();
expect(screen.getByText(/Do the second thing/)).toBeVisible();
expect(screen.getByText(/Third Achievement/)).toBeVisible();
expect(screen.getByText(/Do the third thing/)).toBeVisible();
});

it('displays the achievements with games', () => {
// ARRANGE
const group = createAchievementGroup({
achievements: [
createAchievement({
title: 'First Achievement',
description: 'Do the first thing',
game: createGame({ title: 'First Game' }),
}),
createAchievement({ title: 'Second Achievement', description: 'Do the second thing' }),
createAchievement({ title: 'Third Achievement', description: 'Do the third thing' }),
],
});

render(<AchievementGroup group={group} showGame={true} />);

// ASSERT
expect(screen.getByText(/First Achievement/)).toBeVisible();
expect(screen.getByText(/First Game/)).toBeVisible();
expect(screen.getByText(/Do the first thing/)).toBeVisible();
expect(screen.getByText(/Second Achievement/)).toBeVisible();
expect(screen.getByText(/Do the second thing/)).toBeVisible();
expect(screen.getByText(/Third Achievement/)).toBeVisible();
expect(screen.getByText(/Do the third thing/)).toBeVisible();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { FC } from 'react';

import { UnlockableAchievementAvatar } from '@/features/achievements/components/UnlockableAchievementAvatar';

interface AchievementGroupProps {
group: App.Community.Data.AchievementGroup;
showGame?: boolean;
}

export const AchievementGroup: FC<AchievementGroupProps> = ({ group, showGame = false }) => {
return (
<div>
<h4>{group.header}</h4>
{group.achievements.map((achievement) => (
<UnlockableAchievementAvatar
key={`ach-${achievement.id}-avatar`}
achievement={achievement}
showGame={showGame}
/>
))}
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './AchievementGroup';
Loading

0 comments on commit face8a7

Please sign in to comment.