Skip to content

Commit

Permalink
feat: migrate game Dev Interest page to React (#2997)
Browse files Browse the repository at this point in the history
  • Loading branch information
wescopeland authored Jan 2, 2025
1 parent 293556b commit 67b9798
Show file tree
Hide file tree
Showing 15 changed files with 502 additions and 164 deletions.
35 changes: 35 additions & 0 deletions app/Platform/Actions/BuildGameInterestedDevelopersDataAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace App\Platform\Actions;

use App\Community\Enums\UserGameListType;
use App\Data\UserData;
use App\Models\Game;
use App\Models\Role;
use App\Models\UserGameListEntry;
use Illuminate\Support\Collection;

class BuildGameInterestedDevelopersDataAction
{
/**
* @return Collection<int, UserData>
*/
public function execute(Game $game): Collection
{
$users = UserGameListEntry::whereType(UserGameListType::Develop)
->where('GameID', $game->id)
->with(['user' => function ($query) {
$query->orderBy('User');
}])
->get()
->filter(fn (UserGameListEntry $entry) => $entry->user
&& ($entry->user->hasRole(Role::DEVELOPER) || $entry->user->hasRole(Role::DEVELOPER_JUNIOR))
)
->values()
->map(fn (UserGameListEntry $entry) => UserData::fromUser($entry->user));

return $users;
}
}
15 changes: 15 additions & 0 deletions app/Platform/Controllers/GameController.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@
use App\Models\Game;
use App\Models\System;
use App\Models\User;
use App\Platform\Actions\BuildGameInterestedDevelopersDataAction;
use App\Platform\Actions\BuildGameListAction;
use App\Platform\Actions\GetRandomGameAction;
use App\Platform\Data\DeveloperInterestPagePropsData;
use App\Platform\Data\GameData;
use App\Platform\Data\GameListPagePropsData;
use App\Platform\Data\SystemData;
use App\Platform\Enums\GameListSortField;
Expand Down Expand Up @@ -158,6 +161,18 @@ public function destroy(Game $game): void
$this->authorize('delete', $game);
}

public function devInterest(Game $game): InertiaResponse
{
$this->authorize('viewDeveloperInterest', $game);

$props = new DeveloperInterestPagePropsData(
game: GameData::fromGame($game)->include('badgeUrl', 'system'),
developers: (new BuildGameInterestedDevelopersDataAction())->execute($game)
);

return Inertia::render('game/[game]/dev-interest', $props);
}

public function random(GameListRequest $request): RedirectResponse
{
$this->authorize('viewAny', Game::class);
Expand Down
21 changes: 21 additions & 0 deletions app/Platform/Data/DeveloperInterestPagePropsData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace App\Platform\Data;

use App\Data\UserData;
use Illuminate\Support\Collection;
use Spatie\LaravelData\Data;
use Spatie\TypeScriptTransformer\Attributes\TypeScript;

#[TypeScript('DeveloperInterestPageProps')]
class DeveloperInterestPagePropsData extends Data
{
public function __construct(
public GameData $game,
/** @var Collection<int, UserData> */
public Collection $developers,
) {
}
}
1 change: 1 addition & 0 deletions app/Platform/RouteServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ protected function mapWebRoutes(): void
});

Route::middleware(['web', 'inertia'])->group(function () {
Route::get('game/{game}/dev-interest', [GameController::class, 'devInterest'])->name('game.dev-interest');
Route::get('game/{game}/hashes', [GameHashController::class, 'index'])->name('game.hashes.index');
Route::get('game/{game}/top-achievers', [GameTopAchieversController::class, 'index'])->name('game.top-achievers.index');

Expand Down
27 changes: 0 additions & 27 deletions app/Platform/Services/GameDevInterestPageService.php

This file was deleted.

23 changes: 23 additions & 0 deletions app/Policies/GamePolicy.php
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,29 @@ public function manageContributionCredit(User $user, Game $game): bool
]);
}

public function viewDeveloperInterest(User $user, Game $game): bool
{
$hasActivePrimaryClaim = $user->loadMissing('achievementSetClaims')
->achievementSetClaims()
->whereGameId($game->id)
->primaryClaim()
->active()
->exists();

// Devs and JrDevs can see the page, but they need to have an
// active primary claim first. Collaborators for the game
// cannot open the page.
if ($hasActivePrimaryClaim) {
return true;
}

// Mods and admins can see everything.
return $user->hasAnyRole([
Role::ADMINISTRATOR,
Role::MODERATOR,
]);
}

private function canDeveloperJuniorUpdateGame(User $user, Game $game): bool
{
// If the user has a DEVELOPER_JUNIOR role, they need to have a claim
Expand Down
4 changes: 4 additions & 0 deletions lang/en_US.json
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,10 @@
"Moderation Comments - {{user}}": "Moderation Comments - {{user}}",
"Moderation Comments": "Moderation Comments",
"Columns": "Columns",
"Developer Interest - {{gameTitle}}": "Developer Interest - {{gameTitle}}",
"Developer Interest": "Developer Interest",
"The following users have added this game to their Want to Develop list:": "The following users have added this game to their Want to Develop list:",
"No users have added this game to their Want to Develop list.": "No users have added this game to their Want to Develop list.",
"Developer Feed": "Developer Feed",
"Unlocks Contributed": "Unlocks Contributed",
"Points Contributed": "Points Contributed",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -807,82 +807,86 @@ describe('Component: CreateAchievementTicketMainRoot', () => {
},
);

it('sends along data from the ?extra query param if that data is provided', async () => {
// ARRANGE
const postSpy = vi.spyOn(axios, 'post').mockResolvedValueOnce({ data: { ticketId: 123 } });
it(
'sends along data from the ?extra query param if that data is provided',
{ timeout: 10_000 },
async () => {
// ARRANGE
const postSpy = vi.spyOn(axios, 'post').mockResolvedValueOnce({ data: { ticketId: 123 } });

const achievement = createAchievement();
const gameHashes = [createGameHash({ name: 'Hash A' }), createGameHash({ name: 'Hash B' })];
const emulators = [
createEmulator({ name: 'Bizhawk' }),
createEmulator({ name: 'RALibRetro' }),
createEmulator({ name: 'RetroArch' }),
];
const achievement = createAchievement();
const gameHashes = [createGameHash({ name: 'Hash A' }), createGameHash({ name: 'Hash B' })];
const emulators = [
createEmulator({ name: 'Bizhawk' }),
createEmulator({ name: 'RALibRetro' }),
createEmulator({ name: 'RetroArch' }),
];

render<App.Platform.Data.CreateAchievementTicketPageProps>(
<CreateAchievementTicketMainRoot />,
{
pageProps: {
achievement,
emulators,
gameHashes,
auth: { user: createAuthenticatedUser({ points: 500 }) },
ziggy: createZiggyProps({
query: {
// !!!!!
extra:
'eyJ0cmlnZ2VyUmljaFByZXNlbmNlIjoi8J+Qukxpbmsg8J+Xuu+4j0RlYXRoIE1vdW50YWluIOKdpO+4jzMvMyDwn5GlMS80IPCfp78wLzQg8J+RuzAvNjAg8J+QnDAvMjQg8J+SgDUg8J+VmTEyOjAwIEFN8J+MmSJ9',
},
}),
render<App.Platform.Data.CreateAchievementTicketPageProps>(
<CreateAchievementTicketMainRoot />,
{
pageProps: {
achievement,
emulators,
gameHashes,
auth: { user: createAuthenticatedUser({ points: 500 }) },
ziggy: createZiggyProps({
query: {
// !!!!!
extra:
'eyJ0cmlnZ2VyUmljaFByZXNlbmNlIjoi8J+Qukxpbmsg8J+Xuu+4j0RlYXRoIE1vdW50YWluIOKdpO+4jzMvMyDwn5GlMS80IPCfp78wLzQg8J+RuzAvNjAg8J+QnDAvMjQg8J+SgDUg8J+VmTEyOjAwIEFN8J+MmSJ9',
},
}),
},
},
},
);
);

// ACT
await userEvent.click(screen.getByRole('combobox', { name: /issue/i }));
await userEvent.click(screen.getByRole('option', { name: /did not trigger/i }));
// ACT
await userEvent.click(screen.getByRole('combobox', { name: /issue/i }));
await userEvent.click(screen.getByRole('option', { name: /did not trigger/i }));

await userEvent.click(screen.getByRole('combobox', { name: /emulator/i }));
await userEvent.click(screen.getByRole('option', { name: /retroarch/i }));
await userEvent.click(screen.getByRole('combobox', { name: /emulator/i }));
await userEvent.click(screen.getByRole('option', { name: /retroarch/i }));

await userEvent.type(screen.getByRole('textbox', { name: /emulator core/i }), 'gambatte');
await userEvent.type(screen.getByRole('textbox', { name: /emulator core/i }), 'gambatte');

await userEvent.click(screen.getByRole('radio', { name: /softcore/i }));
await userEvent.click(screen.getByText(/softcore/i));
await userEvent.click(screen.getByRole('radio', { name: /softcore/i }));
await userEvent.click(screen.getByText(/softcore/i));

await userEvent.click(screen.getByRole('combobox', { name: /supported game file/i }));
await userEvent.click(screen.getByRole('option', { name: /hash a/i }));
await userEvent.click(screen.getByRole('combobox', { name: /supported game file/i }));
await userEvent.click(screen.getByRole('option', { name: /hash a/i }));

await userEvent.type(
screen.getByRole('textbox', { name: /description/i }),
'Something is very wrong with this achievement. I tried many things and it just wont unlock. Help.',
);
await userEvent.type(
screen.getByRole('textbox', { name: /description/i }),
'Something is very wrong with this achievement. I tried many things and it just wont unlock. Help.',
);

await userEvent.click(screen.getByRole('button', { name: /submit/i }));
await userEvent.click(screen.getByRole('button', { name: /submit/i }));

// ASSERT
await waitFor(
() => {
expect(postSpy).toHaveBeenCalledOnce();
},
{ timeout: 6000 },
);
// ASSERT
await waitFor(
() => {
expect(postSpy).toHaveBeenCalledOnce();
},
{ timeout: 6000 },
);

expect(postSpy).toHaveBeenCalledWith(['api.ticket.store'], {
core: 'gambatte',
description:
'Something is very wrong with this achievement. I tried many things and it just wont unlock. Help.',
emulator: 'RetroArch',
emulatorVersion: null,
extra:
'eyJ0cmlnZ2VyUmljaFByZXNlbmNlIjoi8J+Qukxpbmsg8J+Xuu+4j0RlYXRoIE1vdW50YWluIOKdpO+4jzMvMyDwn5GlMS80IPCfp78wLzQg8J+RuzAvNjAg8J+QnDAvMjQg8J+SgDUg8J+VmTEyOjAwIEFN8J+MmSJ9',
gameHashId: gameHashes[0].id,
issue: 2,
mode: 'softcore',
ticketableId: achievement.id,
ticketableModel: 'achievement',
});
});
expect(postSpy).toHaveBeenCalledWith(['api.ticket.store'], {
core: 'gambatte',
description:
'Something is very wrong with this achievement. I tried many things and it just wont unlock. Help.',
emulator: 'RetroArch',
emulatorVersion: null,
extra:
'eyJ0cmlnZ2VyUmljaFByZXNlbmNlIjoi8J+Qukxpbmsg8J+Xuu+4j0RlYXRoIE1vdW50YWluIOKdpO+4jzMvMyDwn5GlMS80IPCfp78wLzQg8J+RuzAvNjAg8J+QnDAvMjQg8J+SgDUg8J+VmTEyOjAwIEFN8J+MmSJ9',
gameHashId: gameHashes[0].id,
issue: 2,
mode: 'softcore',
ticketableId: achievement.id,
ticketableModel: 'achievement',
});
},
);

it('given the user is using a non-English locale, shows a warning about their ticket description', () => {
// ARRANGE
Expand Down
Loading

0 comments on commit 67b9798

Please sign in to comment.