From 3c9ae474988619fcbde63130454603c9e1ae9c31 Mon Sep 17 00:00:00 2001 From: Wes Copeland Date: Mon, 2 Sep 2024 19:25:41 -0400 Subject: [PATCH 1/7] refactor: migrate the Random Set route to a controller method (#2665) --- app/Platform/Controllers/GameController.php | 13 ++++++++++++ app/Platform/RouteServiceProvider.php | 2 ++ resources/views/pages/game/random.blade.php | 23 --------------------- 3 files changed, 15 insertions(+), 23 deletions(-) delete mode 100644 resources/views/pages/game/random.blade.php diff --git a/app/Platform/Controllers/GameController.php b/app/Platform/Controllers/GameController.php index d154a1eadb..1a7813ff19 100644 --- a/app/Platform/Controllers/GameController.php +++ b/app/Platform/Controllers/GameController.php @@ -6,6 +6,7 @@ use App\Http\Controller; use App\Models\Game; +use App\Models\System; use App\Platform\Requests\GameRequest; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; @@ -106,4 +107,16 @@ public function destroy(Game $game): void { $this->authorize('delete', $game); } + + public function random(): RedirectResponse + { + $this->authorize('viewAny', Game::class); + + $randomGameWithAchievements = Game::whereNotIn('ConsoleID', System::getNonGameSystems()) + ->where('achievements_published', '>=', 6) + ->inRandomOrder() + ->firstOrFail(); + + return redirect(route('game.show', ['game' => $randomGameWithAchievements])); + } } diff --git a/app/Platform/RouteServiceProvider.php b/app/Platform/RouteServiceProvider.php index d6ebb6313a..e23f30d18a 100755 --- a/app/Platform/RouteServiceProvider.php +++ b/app/Platform/RouteServiceProvider.php @@ -6,6 +6,7 @@ use App\Models\GameHash; use App\Platform\Controllers\AchievementController; +use App\Platform\Controllers\GameController; use App\Platform\Controllers\GameHashController; use App\Platform\Controllers\PlayerAchievementController; use App\Platform\Controllers\PlayerGameController; @@ -63,6 +64,7 @@ protected function mapWebRoutes(): void // Route::get('game/{game}/badges', [GameBadgeController::class, 'index'])->name('game.badge.index'); // Route::get('game/{game}/assets', [GameAssetsController::class, 'index'])->name('game.asset.index'); // Route::get('game/{game}/players', [GamePlayerController::class, 'index'])->name('game.player.index'); + Route::get('game/random', [GameController::class, 'random'])->name('game.random'); // Route::get('create', CreateController::class)->name('create'); // Route::resource('developers', DeveloperController::class)->only('index'); diff --git a/resources/views/pages/game/random.blade.php b/resources/views/pages/game/random.blade.php deleted file mode 100644 index 14f4fa3d41..0000000000 --- a/resources/views/pages/game/random.blade.php +++ /dev/null @@ -1,23 +0,0 @@ -where('achievements_published', '>=', 6) - ->inRandomOrder() - ->firstOrFail(); - - return redirect(route('game.show', ['game' => $randomGameWithAchievements])); -}); - -?> From 89782c41fe0c84178f763dd2885c013775330f42 Mon Sep 17 00:00:00 2001 From: Wes Copeland Date: Tue, 3 Sep 2024 16:08:31 -0400 Subject: [PATCH 2/7] fix: generate api key after user email verification (#2672) --- app/Helpers/database/user-email-verify.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/Helpers/database/user-email-verify.php b/app/Helpers/database/user-email-verify.php index 99044d499d..3c33d9d202 100644 --- a/app/Helpers/database/user-email-verify.php +++ b/app/Helpers/database/user-email-verify.php @@ -57,11 +57,12 @@ function validateEmailVerificationToken(string $emailCookie, ?string &$user): bo $response = SetAccountPermissionsJSON('Server', Permissions::Moderator, $user->username, Permissions::Registered); if ($response['Success']) { static_addnewregistereduser($user->username); - generateAPIKey($user->username); $user->email_verified_at = Carbon::now(); $user->save(); + generateAPIKey($user->username); + // SUCCESS: validated email address for $user return true; } From c590f7f9c7a90de8e02ae1c6caf53d33befade2a Mon Sep 17 00:00:00 2001 From: Wes Copeland Date: Tue, 3 Sep 2024 16:56:56 -0400 Subject: [PATCH 3/7] feat: set up basic Inertia l10n/i18n, use lang strings on `/forums/recent-posts` (#2649) --- .gitignore | 1 + lang/en_US.json | 12 +++ package-lock.json | 30 +++++++ package.json | 1 + resources/js/app.tsx | 15 +++- .../AppProviders/AppProviders.test.tsx | 13 +++ .../components/AppProviders/AppProviders.tsx | 18 ++++ .../common/components/AppProviders/index.ts | 1 + .../AggregateRecentPostLinks.tsx | 7 +- .../RecentPostsBreadcrumbs.tsx | 7 +- .../RecentPostsCards/RecentPostsCards.tsx | 5 +- .../RecentPostsMainRoot.tsx | 5 +- .../RecentPostsPagination.tsx | 9 +- .../RecentPostsTable/RecentPostsTable.tsx | 9 +- resources/js/ssr.tsx | 7 +- resources/js/test/setup.tsx | 84 ++++++++++++++++++- vite.config.ts | 2 + 17 files changed, 211 insertions(+), 15 deletions(-) create mode 100644 lang/en_US.json create mode 100644 resources/js/common/components/AppProviders/AppProviders.test.tsx create mode 100644 resources/js/common/components/AppProviders/AppProviders.tsx create mode 100644 resources/js/common/components/AppProviders/index.ts diff --git a/.gitignore b/.gitignore index b0ba9275d0..0551a32370 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,4 @@ yarn-error.log .phpunit.result.cache _ide_helper.php _ide_helper_models.php +lang/php_*.json \ No newline at end of file diff --git a/lang/en_US.json b/lang/en_US.json new file mode 100644 index 0000000000..dc60506839 --- /dev/null +++ b/lang/en_US.json @@ -0,0 +1,12 @@ +{ + ":count posts in the last 24 hours": ":count posts in the last 24 hours", + ":count posts in the last 7 days": ":count posts in the last 7 days", + "Additional Posts": "Additional Posts", + "Forum Index": "Forum Index", + "in": "in", + "Last Post By": "Last Post By", + "Message": "Message", + "Next :count": "Next :count", + "Previous :count": "Previous :count", + "Recent Posts": "Recent Posts" +} diff --git a/package-lock.json b/package-lock.json index 752e3cdb2c..b0609a2f8d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "clsx": "^2.1.1", "dayjs": "^1.11.11", "js-cookie": "^3.0.5", + "laravel-react-i18n": "^2.0.4", "linkify-html": "^4.1.3", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -8095,6 +8096,20 @@ "node": ">=10" } }, + "node_modules/laravel-react-i18n": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/laravel-react-i18n/-/laravel-react-i18n-2.0.4.tgz", + "integrity": "sha512-gMQVUK9mjM4SXhuiXg5rLNIUslnblB3v3anqzT+/NzodRFnSL6nWDTjpo0Sc8G+WJ1WZxmLLLgaoHXSTk2ZLUQ==", + "license": "MIT", + "dependencies": { + "php-array-reader": "^1.3.5", + "react": "^18.2.0" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=9.0.0" + } + }, "node_modules/laravel-vite-plugin": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-1.0.4.tgz", @@ -8808,6 +8823,21 @@ "node": ">= 14.16" } }, + "node_modules/php-array-reader": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/php-array-reader/-/php-array-reader-1.3.5.tgz", + "integrity": "sha512-jnFhTzSDBHmTJFGmhrMG9vZ+mKBkPm8egcsshW3eSdEyoqthVDUStc3w85aDD4InK0h4LoqP0nd3/xGad2zapA==", + "license": "MIT", + "dependencies": { + "php-parser": "^3.0.3" + } + }, + "node_modules/php-parser": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/php-parser/-/php-parser-3.1.5.tgz", + "integrity": "sha512-jEY2DcbgCm5aclzBdfW86GM6VEIWcSlhTBSHN1qhJguVePlYe28GhwS0yoeLYXpM2K8y6wzLwrbq814n2PHSoQ==", + "license": "BSD-3-Clause" + }, "node_modules/picocolors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", diff --git a/package.json b/package.json index a51eccc656..d1390d36e3 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "clsx": "^2.1.1", "dayjs": "^1.11.11", "js-cookie": "^3.0.5", + "laravel-react-i18n": "^2.0.4", "linkify-html": "^4.1.3", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/resources/js/app.tsx b/resources/js/app.tsx index 13d534454d..0666bf4598 100644 --- a/resources/js/app.tsx +++ b/resources/js/app.tsx @@ -2,6 +2,8 @@ import { createInertiaApp } from '@inertiajs/react'; import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'; import { createRoot, hydrateRoot } from 'react-dom/client'; +import { AppProviders } from './common/components/AppProviders'; + const appName = import.meta.env.APP_NAME || 'RetroAchievements'; createInertiaApp({ @@ -12,12 +14,21 @@ createInertiaApp({ setup({ el, App, props }) { if (import.meta.env.DEV) { - createRoot(el).render(); + createRoot(el).render( + + + , + ); return; } - hydrateRoot(el, ); + hydrateRoot( + el, + + + , + ); }, progress: { diff --git a/resources/js/common/components/AppProviders/AppProviders.test.tsx b/resources/js/common/components/AppProviders/AppProviders.test.tsx new file mode 100644 index 0000000000..c82dadbf43 --- /dev/null +++ b/resources/js/common/components/AppProviders/AppProviders.test.tsx @@ -0,0 +1,13 @@ +import { render } from '@/test'; + +import { AppProviders } from './AppProviders'; + +describe('Component: AppProviders', () => { + it('renders without crashing', () => { + // ARRANGE + const { container } = render(content, { wrapper: () => <> }); + + // ASSERT + expect(container).toBeTruthy(); + }); +}); diff --git a/resources/js/common/components/AppProviders/AppProviders.tsx b/resources/js/common/components/AppProviders/AppProviders.tsx new file mode 100644 index 0000000000..8edadd06fe --- /dev/null +++ b/resources/js/common/components/AppProviders/AppProviders.tsx @@ -0,0 +1,18 @@ +import { LaravelReactI18nProvider } from 'laravel-react-i18n'; +import type { FC, ReactNode } from 'react'; + +interface AppProvidersProps { + children: ReactNode; +} + +export const AppProviders: FC = ({ children }) => { + return ( + + {children} + + ); +}; diff --git a/resources/js/common/components/AppProviders/index.ts b/resources/js/common/components/AppProviders/index.ts new file mode 100644 index 0000000000..4dfaf720df --- /dev/null +++ b/resources/js/common/components/AppProviders/index.ts @@ -0,0 +1 @@ +export * from './AppProviders'; diff --git a/resources/js/features/forums/components/RecentPostsMainRoot/AggregateRecentPostLinks/AggregateRecentPostLinks.tsx b/resources/js/features/forums/components/RecentPostsMainRoot/AggregateRecentPostLinks/AggregateRecentPostLinks.tsx index f960e4930b..55a151c39b 100644 --- a/resources/js/features/forums/components/RecentPostsMainRoot/AggregateRecentPostLinks/AggregateRecentPostLinks.tsx +++ b/resources/js/features/forums/components/RecentPostsMainRoot/AggregateRecentPostLinks/AggregateRecentPostLinks.tsx @@ -1,3 +1,4 @@ +import { useLaravelReactI18n } from 'laravel-react-i18n'; import type { FC } from 'react'; import type { RecentActiveForumTopic } from '@/features/forums/models'; @@ -7,6 +8,8 @@ interface AggregateRecentPostLinksProps { } export const AggregateRecentPostLinks: FC = ({ topic }) => { + const { t } = useLaravelReactI18n(); + const { commentCount24h, commentCount7d, oldestComment24hId, oldestComment7dId, id } = topic; if (!commentCount7d || commentCount7d <= 1) { @@ -21,13 +24,13 @@ export const AggregateRecentPostLinks: FC = ({ to diff --git a/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsBreadcrumbs/RecentPostsBreadcrumbs.tsx b/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsBreadcrumbs/RecentPostsBreadcrumbs.tsx index 276a1485db..0c3788cd09 100644 --- a/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsBreadcrumbs/RecentPostsBreadcrumbs.tsx +++ b/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsBreadcrumbs/RecentPostsBreadcrumbs.tsx @@ -1,3 +1,4 @@ +import { useLaravelReactI18n } from 'laravel-react-i18n'; import type { FC } from 'react'; import { @@ -10,18 +11,20 @@ import { } from '@/common/components/+vendor/BaseBreadcrumb'; export const RecentPostsBreadcrumbs: FC = () => { + const { t } = useLaravelReactI18n(); + return (
- Forum Index + {t('Forum Index')} - Recent Posts + {t('Recent Posts')} diff --git a/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsCards/RecentPostsCards.tsx b/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsCards/RecentPostsCards.tsx index 378540d4fe..bc8d69b6d4 100644 --- a/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsCards/RecentPostsCards.tsx +++ b/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsCards/RecentPostsCards.tsx @@ -1,4 +1,5 @@ import { usePage } from '@inertiajs/react'; +import { useLaravelReactI18n } from 'laravel-react-i18n'; import type { FC } from 'react'; import { UserAvatar } from '@/common/components/UserAvatar'; @@ -8,6 +9,8 @@ import { AggregateRecentPostLinks } from '../AggregateRecentPostLinks'; import { PostTimestamp } from '../PostTimestamp'; export const RecentPostsCards: FC = () => { + const { t } = useLaravelReactI18n(); + const { props } = usePage(); const { auth, paginatedTopics } = props; @@ -33,7 +36,7 @@ export const RecentPostsCards: FC = () => {

- in{' '} + {t('in')}{' '} diff --git a/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsMainRoot.tsx b/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsMainRoot.tsx index c666ed9548..219e5d8d69 100644 --- a/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsMainRoot.tsx +++ b/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsMainRoot.tsx @@ -1,3 +1,4 @@ +import { useLaravelReactI18n } from 'laravel-react-i18n'; import type { FC } from 'react'; import { RecentPostsBreadcrumbs } from './RecentPostsBreadcrumbs'; @@ -6,11 +7,13 @@ import { RecentPostsPagination } from './RecentPostsPagination'; import { RecentPostsTable } from './RecentPostsTable'; export const RecentPostsMainRoot: FC = () => { + const { t } = useLaravelReactI18n(); + return (

-

Recent Posts

+

{t('Recent Posts')}

diff --git a/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsPagination/RecentPostsPagination.tsx b/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsPagination/RecentPostsPagination.tsx index d7b341dbc7..dd79510a2a 100644 --- a/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsPagination/RecentPostsPagination.tsx +++ b/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsPagination/RecentPostsPagination.tsx @@ -1,4 +1,5 @@ import { usePage } from '@inertiajs/react'; +import { useLaravelReactI18n } from 'laravel-react-i18n'; import type { FC } from 'react'; import { @@ -11,6 +12,8 @@ import { import type { RecentPostsPageProps } from '@/features/forums/models'; export const RecentPostsPagination: FC = () => { + const { t } = useLaravelReactI18n(); + const { paginatedTopics } = usePage().props; const { @@ -28,14 +31,16 @@ export const RecentPostsPagination: FC = () => { {previousPageUrl ? ( - Previous {perPage} + {t('Previous :count', { count: perPage })} ) : null} {nextPageUrl ? ( - Next {perPage} + + {t('Next :count', { count: perPage })} + ) : null} diff --git a/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsTable/RecentPostsTable.tsx b/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsTable/RecentPostsTable.tsx index eb039a3ed9..afb52f14a5 100644 --- a/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsTable/RecentPostsTable.tsx +++ b/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsTable/RecentPostsTable.tsx @@ -1,4 +1,5 @@ import { usePage } from '@inertiajs/react'; +import { useLaravelReactI18n } from 'laravel-react-i18n'; import type { FC } from 'react'; import { UserAvatar } from '@/common/components/UserAvatar'; @@ -8,6 +9,8 @@ import { AggregateRecentPostLinks } from '../AggregateRecentPostLinks'; import { PostTimestamp } from '../PostTimestamp'; export const RecentPostsTable: FC = () => { + const { t } = useLaravelReactI18n(); + const { props } = usePage(); const { auth, paginatedTopics } = props; @@ -16,9 +19,9 @@ export const RecentPostsTable: FC = () => { - - - + + + diff --git a/resources/js/ssr.tsx b/resources/js/ssr.tsx index d245677e70..3fa495f139 100644 --- a/resources/js/ssr.tsx +++ b/resources/js/ssr.tsx @@ -7,6 +7,7 @@ import ReactDOMServer from 'react-dom/server'; import type { RouteName, RouteParams } from 'ziggy-js'; import { route } from '../../vendor/tightenco/ziggy'; +import { AppProviders } from './common/components/AppProviders/AppProviders'; const appName = import.meta.env.APP_NAME || 'RetroAchievements'; @@ -30,7 +31,11 @@ createServer((page) => location: new URL(page.props.ziggy.location), }); - return ; + return ( + + + + ); }, }), ); diff --git a/resources/js/test/setup.tsx b/resources/js/test/setup.tsx index 4ac3a8e289..0e27c34efc 100644 --- a/resources/js/test/setup.tsx +++ b/resources/js/test/setup.tsx @@ -4,6 +4,8 @@ import * as InertiajsReactModule from '@inertiajs/react'; import { render as defaultRender } from '@testing-library/react'; import type { ReactNode } from 'react'; +import { AppProviders } from '@/common/components/AppProviders'; + export * from '@testing-library/react'; vi.mock('@inertiajs/react', () => ({ @@ -11,6 +13,86 @@ vi.mock('@inertiajs/react', () => ({ usePage: vi.fn(), })); +/** + * laravel-react-i18n does not support Vitest, and without any intervention + * it will believe it's being rendered in the browser and cause React to + * output no DOM. Rather than trying to hack laravel-react-i18n to fix this, + * it's better to just mock the functions we need to test basic UI functionality. + */ +vi.mock('laravel-react-i18n', () => ({ + __esModule: true, + + LaravelReactI18nProvider: ({ children }: any) => <>{children}, + + useLaravelReactI18n: () => ({ + loading: false, + currentLocale: () => 'en_US', + + /** + * t('Welcome!'); // "Welcome" + * t('Welcome, :name!', { name: 'Francisco' }); // "Welcome, Francisco!" + * t(':count apples', { count: 2 }); // "2 apples" + */ + t: (key: string, replacements: Record) => { + return key.replace(/:([a-zA-Z]+)/g, (_, match) => { + const replacementKey = Object.keys(replacements).find( + (key) => key.toLowerCase() === match.toLowerCase(), + ); + + const replacement = replacementKey ? replacements[replacementKey] : match; + + return replacement !== undefined ? String(replacement) : match; + }); + }, + + /** + * tChoice('{0} There are none|[1,19] There are some|[20,*] There are many', 0); // "There are none" + * tChoice('{0} There are none|[1,19] There are some|[20,*] There are many', 4); // "There are some" + * tChoice('{0} There are none|[1,19] There are some|[20,*] There are many', 20); // "There are many" + * tChoice('There is one apple|There are many apples', 1); // "There is one apple" + * tChoice('There is one apple|There are many apples', 4); // "There are many apples" + * tChoice('{1} :count minute ago|[2,*] :count minutes ago', 1); // "1 minute ago" + * tChoice('{1} :count minute ago|[2,*] :count minutes ago', 4); // "4 minutes ago" + */ + tChoice: (key: string, count: number, replacements: Record = {}) => { + const choices = key.split('|'); + let selectedChoice = choices[choices.length - 1]; // Default to the last choice (plural) + + for (let i = 0; i < choices.length; i++) { + const match = choices[i].match(/^\{(\d+)\}\s|^\[(\d+),(\*|\d+)\]\s/); + if (match) { + const exact = match[1] !== undefined ? parseInt(match[1]) : undefined; + const rangeStart = match[2] !== undefined ? parseInt(match[2]) : undefined; + const rangeEnd = + match[3] === '*' ? Infinity : match[3] !== undefined ? parseInt(match[3]) : undefined; + + if (exact !== undefined && count === exact) { + selectedChoice = choices[i].replace(match[0], '').trim(); + break; + } else if ( + rangeEnd && + rangeStart !== undefined && + count >= rangeStart && + count <= rangeEnd + ) { + selectedChoice = choices[i].replace(match[0], '').trim(); + break; + } + } else if (count === 1 && choices.length === 2) { + selectedChoice = choices[0].trim(); + break; + } + } + + return selectedChoice.replace(/:([a-zA-Z]+)/g, (_, match) => { + const replacement = replacements[match] || count; + + return replacement; + }); + }, + }), +})); + /* |-------------------------------------------------------------------------- | Test Suite Custom Render Method @@ -41,7 +123,7 @@ export function render(ui: RenderUI, { wrapper, pageProps = {}, ...options }: Re })); if (!wrapper) { - wrapper = ({ children }: WrapperProps) => <>{children}; + wrapper = ({ children }: WrapperProps) => {children}; } return { diff --git a/vite.config.ts b/vite.config.ts index 95aba92eea..ffbebb2c1d 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,6 +2,7 @@ import react from '@vitejs/plugin-react'; import { existsSync, readFileSync } from 'fs'; +import i18n from 'laravel-react-i18n/vite'; import laravel from 'laravel-vite-plugin'; import { homedir } from 'os'; import { resolve } from 'path'; @@ -37,6 +38,7 @@ export default defineConfig(({ mode, isSsrBuild }) => { refresh: ['resources/views/**'], }), react(), + i18n(), ], ssr: { From daa68932b0fd778e0d3400e7a371a0f63512bec7 Mon Sep 17 00:00:00 2001 From: Wes Copeland Date: Tue, 3 Sep 2024 17:16:26 -0400 Subject: [PATCH 4/7] feat: migrate Supported Game Files to React (#2659) --- .eslintrc.yaml | 1 + app/Data/UserPermissionsData.php | 26 +++++ .../Controllers/GameHashController.php | 24 +++- app/Platform/Data/GameData.php | 34 ++++++ app/Platform/Data/GameHashData.php | 48 ++++++++ app/Platform/Data/GameHashLabelData.php | 39 +++++++ app/Platform/Data/GameHashesPagePropsData.php | 23 ++++ app/Platform/Data/SystemData.php | 32 ++++++ app/Platform/RouteServiceProvider.php | 4 + app/Policies/GameHashPolicy.php | 9 +- .../components/+vendor/BaseBreadcrumb.tsx | 4 +- .../common/components/+vendor/BaseButton.tsx | 15 +-- .../js/common/components/Embed/Embed.test.tsx | 21 ++++ .../js/common/components/Embed/Embed.tsx | 15 +++ resources/js/common/components/Embed/index.ts | 1 + .../components/GameAvatar/GameAvatar.test.tsx | 80 +++++++++++++ .../components/GameAvatar/GameAvatar.tsx | 49 ++++++++ .../js/common/components/GameAvatar/index.ts | 1 + .../components/UserAvatar/UserAvatar.tsx | 5 +- .../js/common/layouts/AppLayout/AppLayout.tsx | 2 +- .../js/common/models/avatar-size.model.ts | 3 + resources/js/common/models/index.ts | 1 + .../utils/buildTrackingClassNames.test.ts | 51 +++++++++ .../common/utils/buildTrackingClassNames.ts | 36 ++++++ .../GameBreadcrumbs/GameBreadcrumbs.test.tsx | 50 +++++++++ .../GameBreadcrumbs/GameBreadcrumbs.tsx | 60 ++++++++++ .../games/components/GameBreadcrumbs/index.ts | 1 + .../GameHeading/GameHeading.test.tsx | 38 +++++++ .../components/GameHeading/GameHeading.tsx | 20 ++++ .../games/components/GameHeading/index.ts | 1 + .../HashesList/HashesList.test.tsx | 78 +++++++++++++ .../HashesMainRoot/HashesList/HashesList.tsx | 43 +++++++ .../HashesList/HashesListItem.tsx | 52 +++++++++ .../HashesMainRoot/HashesList/index.ts | 1 + .../HashesMainRoot/HashesMainRoot.test.tsx | 34 ++++++ .../HashesMainRoot/HashesMainRoot.tsx | 74 ++++++++++++ .../games/components/HashesMainRoot/index.ts | 1 + resources/js/pages/game/[game]/hashes.tsx | 26 +++++ resources/js/ssr.tsx | 4 +- resources/js/test/factories/createGame.ts | 12 ++ resources/js/test/factories/createGameHash.ts | 19 ++++ .../js/test/factories/createGameHashLabel.ts | 17 +++ resources/js/test/factories/createSystem.ts | 10 ++ resources/js/test/factories/index.ts | 5 + resources/js/test/setup.tsx | 16 ++- resources/js/types/generated.d.ts | 35 +++++- resources/js/ziggy.d.ts | 14 +-- .../game/link-buttons/index.blade.php | 2 +- .../hash-listing.blade.php | 40 ------- .../pages/game/[game]/hashes/index.blade.php | 105 ------------------ 50 files changed, 1100 insertions(+), 182 deletions(-) create mode 100644 app/Data/UserPermissionsData.php create mode 100644 app/Platform/Data/GameData.php create mode 100644 app/Platform/Data/GameHashData.php create mode 100644 app/Platform/Data/GameHashLabelData.php create mode 100644 app/Platform/Data/GameHashesPagePropsData.php create mode 100644 app/Platform/Data/SystemData.php create mode 100644 resources/js/common/components/Embed/Embed.test.tsx create mode 100644 resources/js/common/components/Embed/Embed.tsx create mode 100644 resources/js/common/components/Embed/index.ts create mode 100644 resources/js/common/components/GameAvatar/GameAvatar.test.tsx create mode 100644 resources/js/common/components/GameAvatar/GameAvatar.tsx create mode 100644 resources/js/common/components/GameAvatar/index.ts create mode 100644 resources/js/common/models/avatar-size.model.ts create mode 100644 resources/js/common/utils/buildTrackingClassNames.test.ts create mode 100644 resources/js/common/utils/buildTrackingClassNames.ts create mode 100644 resources/js/features/games/components/GameBreadcrumbs/GameBreadcrumbs.test.tsx create mode 100644 resources/js/features/games/components/GameBreadcrumbs/GameBreadcrumbs.tsx create mode 100644 resources/js/features/games/components/GameBreadcrumbs/index.ts create mode 100644 resources/js/features/games/components/GameHeading/GameHeading.test.tsx create mode 100644 resources/js/features/games/components/GameHeading/GameHeading.tsx create mode 100644 resources/js/features/games/components/GameHeading/index.ts create mode 100644 resources/js/features/games/components/HashesMainRoot/HashesList/HashesList.test.tsx create mode 100644 resources/js/features/games/components/HashesMainRoot/HashesList/HashesList.tsx create mode 100644 resources/js/features/games/components/HashesMainRoot/HashesList/HashesListItem.tsx create mode 100644 resources/js/features/games/components/HashesMainRoot/HashesList/index.ts create mode 100644 resources/js/features/games/components/HashesMainRoot/HashesMainRoot.test.tsx create mode 100644 resources/js/features/games/components/HashesMainRoot/HashesMainRoot.tsx create mode 100644 resources/js/features/games/components/HashesMainRoot/index.ts create mode 100644 resources/js/pages/game/[game]/hashes.tsx create mode 100644 resources/js/test/factories/createGame.ts create mode 100644 resources/js/test/factories/createGameHash.ts create mode 100644 resources/js/test/factories/createGameHashLabel.ts create mode 100644 resources/js/test/factories/createSystem.ts delete mode 100644 resources/views/components/supported-game-files/hash-listing.blade.php delete mode 100644 resources/views/pages/game/[game]/hashes/index.blade.php diff --git a/.eslintrc.yaml b/.eslintrc.yaml index 27ef3600fa..a473793862 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -102,6 +102,7 @@ rules: react/no-unescaped-entities: off react/prop-types: off react/react-in-jsx-scope: off + react/jsx-no-target-blank: off # we don't support the old browsers this rule tries to protect # disable some of the more aggressive unicorn rules unicorn/filename-case: off diff --git a/app/Data/UserPermissionsData.php b/app/Data/UserPermissionsData.php new file mode 100644 index 0000000000..be495428de --- /dev/null +++ b/app/Data/UserPermissionsData.php @@ -0,0 +1,26 @@ + $user ? $user->can('manage', \App\Models\GameHash::class) : false), + ); + } +} diff --git a/app/Platform/Controllers/GameHashController.php b/app/Platform/Controllers/GameHashController.php index 2e0714f0aa..af5996149d 100644 --- a/app/Platform/Controllers/GameHashController.php +++ b/app/Platform/Controllers/GameHashController.php @@ -5,13 +5,19 @@ namespace App\Platform\Controllers; use App\Community\Enums\ArticleType; +use App\Data\UserPermissionsData; use App\Http\Controller; +use App\Models\Game; use App\Models\GameHash; use App\Models\User; -use Illuminate\Contracts\View\View; +use App\Platform\Data\GameData; +use App\Platform\Data\GameHashData; +use App\Platform\Data\GameHashesPagePropsData; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; +use Inertia\Inertia; +use Inertia\Response as InertiaResponse; class GameHashController extends Controller { @@ -20,12 +26,17 @@ protected function resourceName(): string return 'game-hash'; } - public function index(Request $request): View + public function index(Request $request, Game $game): InertiaResponse { $this->authorize('viewAny', $this->resourceClass()); - return view('resource.index') - ->with('resource', $this->resourceName()); + $gameData = GameData::fromGame($game)->include('badgeUrl', 'forumTopicId', 'system'); + $hashes = GameHashData::fromCollection($game->hashes); + $can = UserPermissionsData::fromUser($request->user())->include('manageGameHashes'); + + $props = new GameHashesPagePropsData($gameData, $hashes, $can); + + return Inertia::render('game/[game]/hashes', $props); } public function show(GameHash $gameHash): void @@ -70,7 +81,10 @@ public function update(Request $request, GameHash $gameHash): JsonResponse } $gameHash->update($updatedAttributes); - $this->logGameHashUpdate($gameHash, $changedAttributes, Auth::user()); + + /** @var User $user */ + $user = Auth::user(); + $this->logGameHashUpdate($gameHash, $changedAttributes, $user); return response()->json(['message' => __('legacy.success.update')]); } diff --git a/app/Platform/Data/GameData.php b/app/Platform/Data/GameData.php new file mode 100644 index 0000000000..e91934c713 --- /dev/null +++ b/app/Platform/Data/GameData.php @@ -0,0 +1,34 @@ +id, + title: $game->title, + badgeUrl: Lazy::create(fn () => $game->badge_url), + forumTopicId: Lazy::create(fn () => $game->ForumTopicID), + system: Lazy::create(fn () => SystemData::fromSystem($game->system)) + ); + } +} diff --git a/app/Platform/Data/GameHashData.php b/app/Platform/Data/GameHashData.php new file mode 100644 index 0000000000..cb71d763bf --- /dev/null +++ b/app/Platform/Data/GameHashData.php @@ -0,0 +1,48 @@ +id, + md5: $gameHash->md5, + name: $gameHash->name, + labels: GameHashLabelData::fromLabelsString($gameHash->labels), + patchUrl: $gameHash->patch_url, + ); + } + + /** + * @param Collection $gameHashes + * @return GameHashData[] + */ + public static function fromCollection(Collection $gameHashes): array + { + return array_map( + fn ($gameHash) => self::fromGameHash($gameHash), + $gameHashes->all() + ); + } +} diff --git a/app/Platform/Data/GameHashLabelData.php b/app/Platform/Data/GameHashLabelData.php new file mode 100644 index 0000000000..977ea9dae6 --- /dev/null +++ b/app/Platform/Data/GameHashLabelData.php @@ -0,0 +1,39 @@ +id, + name: $system->name, + nameFull: Lazy::create(fn () => $system->name_full), + nameShort: Lazy::create(fn () => $system->name_short), + ); + } +} diff --git a/app/Platform/RouteServiceProvider.php b/app/Platform/RouteServiceProvider.php index e23f30d18a..3d1a74e2f9 100755 --- a/app/Platform/RouteServiceProvider.php +++ b/app/Platform/RouteServiceProvider.php @@ -42,6 +42,10 @@ public function map(): void protected function mapWebRoutes(): void { Route::middleware(['web', 'csp'])->group(function () { + Route::middleware(['inertia'])->group(function () { + Route::get('game/{game}/hashes', [GameHashController::class, 'index'])->name('game.hashes.index'); + }); + // Route::get('achievement/{achievement}{slug?}', [AchievementController::class, 'show'])->name('achievement.show'); // Route::resource('achievements', AchievementController::class)->only('index')->names(['index' => 'achievement.index']); // Route::get( diff --git a/app/Policies/GameHashPolicy.php b/app/Policies/GameHashPolicy.php index bce9844f9a..fd80f1e776 100644 --- a/app/Policies/GameHashPolicy.php +++ b/app/Policies/GameHashPolicy.php @@ -4,7 +4,6 @@ namespace App\Policies; -use App\Enums\Permissions; use App\Models\GameHash; use App\Models\Role; use App\Models\User; @@ -20,8 +19,7 @@ public function manage(User $user): bool Role::GAME_HASH_MANAGER, Role::DEVELOPER_STAFF, Role::DEVELOPER, - ]) - || $user->getAttribute('Permissions') >= Permissions::Developer; + ]); } public function viewAny(?User $user): bool @@ -47,8 +45,9 @@ public function update(User $user): bool { return $user->hasAnyRole([ Role::GAME_HASH_MANAGER, - ]) - || $user->getAttribute('Permissions') >= Permissions::Developer; + Role::DEVELOPER_STAFF, + Role::DEVELOPER, + ]); } public function delete(User $user, GameHash $gameHash): bool diff --git a/resources/js/common/components/+vendor/BaseBreadcrumb.tsx b/resources/js/common/components/+vendor/BaseBreadcrumb.tsx index 56959d2e72..a760c7b9a5 100644 --- a/resources/js/common/components/+vendor/BaseBreadcrumb.tsx +++ b/resources/js/common/components/+vendor/BaseBreadcrumb.tsx @@ -51,7 +51,7 @@ const BaseBreadcrumbLink = forwardRef< return ( ); @@ -65,7 +65,7 @@ const BaseBreadcrumbPage = forwardRef ), diff --git a/resources/js/common/components/+vendor/BaseButton.tsx b/resources/js/common/components/+vendor/BaseButton.tsx index 9884875d46..1948d227b1 100644 --- a/resources/js/common/components/+vendor/BaseButton.tsx +++ b/resources/js/common/components/+vendor/BaseButton.tsx @@ -8,17 +8,18 @@ import { cn } from '@/utils/cn'; const baseButtonVariants = cva( [ - 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium light:ring-offset-white', + 'inline-flex items-center justify-center whitespace-nowrap rounded text-sm font-medium light:ring-offset-white', 'focus-visible:outline-none focus-visible:ring-2 light:focus-visible:ring-neutral-950 focus-visible:ring-offset-2', - 'disabled:pointer-events-none disabled:opacity-50 ring-offset-neutral-950 focus-visible:ring-neutral-300', + 'disabled:pointer-events-none disabled:opacity-50', + 'ring-offset-neutral-950 focus-visible:ring-neutral-300', + 'lg:active:translate-y-[1px] lg:active:scale-[0.98] lg:transition-transform lg:duration-100', ], { variants: { variant: { default: - 'light:bg-neutral-900 light:text-neutral-50 light:hover:bg-neutral-900/90 bg-neutral-50 text-neutral-900 hover:bg-neutral-50/90', - destructive: - 'light:bg-red-500 light:text-neutral-50 light:hover:bg-red-500/90 bg-red-900 text-neutral-50 hover:bg-red-900/90', + 'bg-embed text-link border border-neutral-700 hover:bg-embed-highlight hover:text-link-hover hover:border-menu-link light:bg-white light:border-link light:text-link light:hover:bg-neutral-100', + destructive: 'bg-embed border btn-danger hover:text-link-hover hover:border-menu-link', outline: 'border light:border-neutral-200 light:bg-white light:hover:bg-neutral-100 light:hover:text-neutral-900 border-neutral-800 bg-neutral-950 hover:bg-neutral-800 hover:text-neutral-50', secondary: @@ -28,8 +29,8 @@ const baseButtonVariants = cva( link: 'light:text-neutral-900 underline-offset-4 hover:underline text-neutral-50', }, size: { - default: 'h-10 px-4 py-2', - sm: 'h-9 rounded-md px-3', + default: 'h-9 px-4 py-2', + sm: 'h-[30px] rounded-md px-3 !text-[13px]', lg: 'h-11 rounded-md px-8', icon: 'h-10 w-10', }, diff --git a/resources/js/common/components/Embed/Embed.test.tsx b/resources/js/common/components/Embed/Embed.test.tsx new file mode 100644 index 0000000000..3add999f75 --- /dev/null +++ b/resources/js/common/components/Embed/Embed.test.tsx @@ -0,0 +1,21 @@ +import { render, screen } from '@/test'; + +import { Embed } from './Embed'; + +describe('Component: Embed', () => { + it('renders without crashing', () => { + // ARRANGE + const { container } = render(stuff); + + // ASSERT + expect(container).toBeTruthy(); + }); + + it('renders children', () => { + // ARRANGE + render(stuff); + + // ASSERT + expect(screen.getByText(/stuff/i)).toBeVisible(); + }); +}); diff --git a/resources/js/common/components/Embed/Embed.tsx b/resources/js/common/components/Embed/Embed.tsx new file mode 100644 index 0000000000..d3ad40c7ab --- /dev/null +++ b/resources/js/common/components/Embed/Embed.tsx @@ -0,0 +1,15 @@ +import type { FC, HTMLAttributes, ReactNode } from 'react'; + +interface EmbedProps extends HTMLAttributes { + children: ReactNode; + + className?: string; +} + +export const Embed: FC = ({ children, className, ...rest }) => { + return ( +
+ {children} +
+ ); +}; diff --git a/resources/js/common/components/Embed/index.ts b/resources/js/common/components/Embed/index.ts new file mode 100644 index 0000000000..8113a2f96c --- /dev/null +++ b/resources/js/common/components/Embed/index.ts @@ -0,0 +1 @@ +export * from './Embed'; diff --git a/resources/js/common/components/GameAvatar/GameAvatar.test.tsx b/resources/js/common/components/GameAvatar/GameAvatar.test.tsx new file mode 100644 index 0000000000..d367d29198 --- /dev/null +++ b/resources/js/common/components/GameAvatar/GameAvatar.test.tsx @@ -0,0 +1,80 @@ +import { render, screen } from '@/test'; +import { createGame } from '@/test/factories'; + +import { GameAvatar } from './GameAvatar'; + +describe('Component: GameAvatar', () => { + it('renders without crashing', () => { + // ARRANGE + const { container } = render(); + + // ASSERT + expect(container).toBeTruthy(); + }); + + it('given a game title, shows the game title on the screen', () => { + // ARRANGE + const game = createGame(); + + render(); + + // ASSERT + expect(screen.getByText(game.title)).toBeVisible(); + }); + + it('given there is no title, still renders successfully', () => { + // ARRANGE + const game = createGame({ title: undefined }); + + render(); + + // ASSERT + expect(screen.getByRole('img', { name: /game/i })).toBeVisible(); + }); + + it('applies the correct size to the image', () => { + // ARRANGE + const game = createGame(); + + render(); + + // ASSERT + const imgEl = screen.getByRole('img'); + + expect(imgEl).toHaveAttribute('width', '8'); + expect(imgEl).toHaveAttribute('height', '8'); + }); + + it('adds card tooltip props by default', () => { + // ARRANGE + const game = createGame({ id: 1 }); + + render(); + + // ASSERT + const anchorEl = screen.getByRole('link'); + + expect(anchorEl).toHaveAttribute( + 'x-data', + "tooltipComponent($el, {dynamicType: 'game', dynamicId: '1', dynamicContext: 'undefined'})", + ); + expect(anchorEl).toHaveAttribute('x-on:mouseover', 'showTooltip($event)'); + expect(anchorEl).toHaveAttribute('x-on:mouseleave', 'hideTooltip'); + expect(anchorEl).toHaveAttribute('x-on:mousemove', 'trackMouseMovement($event)'); + }); + + it('does not add card tooltip props when `hasTooltip` is false', () => { + // ARRANGE + const game = createGame({ id: 1 }); + + render(); + + // ASSERT + const anchorEl = screen.getByRole('link'); + + expect(anchorEl).not.toHaveAttribute('x-data'); + expect(anchorEl).not.toHaveAttribute('x-on:mouseover'); + expect(anchorEl).not.toHaveAttribute('x-on:mouseleave'); + expect(anchorEl).not.toHaveAccessibleDescription('x-on:mousemove'); + }); +}); diff --git a/resources/js/common/components/GameAvatar/GameAvatar.tsx b/resources/js/common/components/GameAvatar/GameAvatar.tsx new file mode 100644 index 0000000000..54208125cf --- /dev/null +++ b/resources/js/common/components/GameAvatar/GameAvatar.tsx @@ -0,0 +1,49 @@ +import type { FC } from 'react'; + +import { useCardTooltip } from '@/common/hooks/useCardTooltip'; +import type { AvatarSize } from '@/common/models'; + +interface GameAvatarProps { + id: number; + + badgeUrl?: string; + hasTooltip?: boolean; + showBadge?: boolean; + showTitle?: boolean; + size?: AvatarSize; + title?: string; +} + +export const GameAvatar: FC = ({ + id, + badgeUrl, + showBadge, + showTitle, + title, + size = 32, + hasTooltip = true, +}) => { + const { cardTooltipProps } = useCardTooltip({ dynamicType: 'game', dynamicId: id }); + + return ( + + {showBadge !== false ? ( + + ) : null} + + {title && showTitle !== false ? {title} : null} + + ); +}; diff --git a/resources/js/common/components/GameAvatar/index.ts b/resources/js/common/components/GameAvatar/index.ts new file mode 100644 index 0000000000..e2a42ee035 --- /dev/null +++ b/resources/js/common/components/GameAvatar/index.ts @@ -0,0 +1 @@ +export * from './GameAvatar'; diff --git a/resources/js/common/components/UserAvatar/UserAvatar.tsx b/resources/js/common/components/UserAvatar/UserAvatar.tsx index ffdfac06e4..5821935519 100644 --- a/resources/js/common/components/UserAvatar/UserAvatar.tsx +++ b/resources/js/common/components/UserAvatar/UserAvatar.tsx @@ -1,14 +1,13 @@ import type { FC } from 'react'; import { useCardTooltip } from '@/common/hooks/useCardTooltip'; +import type { AvatarSize } from '@/common/models'; interface UserAvatarProps { displayName: string | null; hasTooltip?: boolean; - // This is strongly typed so we don't wind up with 100 different possible sizes. - // If possible, use one of these sane defaults. Only add another one if necessary. - size?: 8 | 16 | 24 | 32 | 64 | 128; + size?: AvatarSize; } export const UserAvatar: FC = ({ displayName, size = 32, hasTooltip = true }) => { diff --git a/resources/js/common/layouts/AppLayout/AppLayout.tsx b/resources/js/common/layouts/AppLayout/AppLayout.tsx index 8bcbe83ae0..daa3ccd19c 100644 --- a/resources/js/common/layouts/AppLayout/AppLayout.tsx +++ b/resources/js/common/layouts/AppLayout/AppLayout.tsx @@ -20,7 +20,7 @@ interface AppLayoutMainProps { } const AppLayoutMain: FC = ({ children }) => { - return
{children}
; + return
{children}
; }; interface AppLayoutSidebarProps { diff --git a/resources/js/common/models/avatar-size.model.ts b/resources/js/common/models/avatar-size.model.ts new file mode 100644 index 0000000000..232b2fcc05 --- /dev/null +++ b/resources/js/common/models/avatar-size.model.ts @@ -0,0 +1,3 @@ +// This is strongly typed so we don't wind up with 100 different possible sizes. +// If possible, use one of these sane defaults. Only add another one if necessary. +export type AvatarSize = 8 | 16 | 24 | 32 | 48 | 64 | 128; diff --git a/resources/js/common/models/index.ts b/resources/js/common/models/index.ts index a9cac77932..4e2d74273a 100644 --- a/resources/js/common/models/index.ts +++ b/resources/js/common/models/index.ts @@ -1,3 +1,4 @@ export * from './app-global-props.model'; export * from './app-page.model'; +export * from './avatar-size.model'; export * from './paginated-data.model'; diff --git a/resources/js/common/utils/buildTrackingClassNames.test.ts b/resources/js/common/utils/buildTrackingClassNames.test.ts new file mode 100644 index 0000000000..639acc6eee --- /dev/null +++ b/resources/js/common/utils/buildTrackingClassNames.test.ts @@ -0,0 +1,51 @@ +import { buildTrackingClassNames } from './buildTrackingClassNames'; + +// It isn't necessary log the warning for an empty customEventName. +global.console.warn = vi.fn(); + +describe('Util: buildTrackingClassNames', () => { + it('is defined', () => { + // ASSERT + expect(buildTrackingClassNames).toBeDefined(); + }); + + it('given only an event name is provided, returns a single classname', () => { + // ACT + const result = buildTrackingClassNames('Download Patch File Click'); + + // ASSERT + expect(result).toEqual('plausible-event-name=Download+Patch+File+Click'); + }); + + it('given an event name and a property are provided, returns multiple classnames', () => { + // ACT + const result = buildTrackingClassNames('Download Patch File Click', { md5: 'abc123' }); + + // ASSERT + expect(result).toEqual( + 'plausible-event-name=Download+Patch+File+Click plausible-event-md5=abc123', + ); + }); + + it('correctly handles multiple properties with different types', () => { + // ACT + const result = buildTrackingClassNames('Submit Form', { + id: 123, + success: true, + description: 'User Submitted Form', + }); + + // ASSERT + expect(result).toEqual( + 'plausible-event-name=Submit+Form plausible-event-id=123 plausible-event-success=true plausible-event-description=User+Submitted+Form', + ); + }); + + it('returns an empty string when neither event name nor properties are provided', () => { + // ACT + const result = buildTrackingClassNames(''); + + // ASSERT + expect(result).toEqual(''); + }); +}); diff --git a/resources/js/common/utils/buildTrackingClassNames.ts b/resources/js/common/utils/buildTrackingClassNames.ts new file mode 100644 index 0000000000..80354d9eaa --- /dev/null +++ b/resources/js/common/utils/buildTrackingClassNames.ts @@ -0,0 +1,36 @@ +/** + * Track a custom Plausible event on click. Optionally accepts custom properties to decorate + * the event with more specificity (see example). + * + * @example + * className={buildTrackingClassNames('Download Patch File', { md5: hash.md5 })} + * className={`px-3 py-4 ${buildTrackingClassNames('Download Patch File', { md5: hash.md5 })}`} + */ +export function buildTrackingClassNames( + customEventName: string, + customProperties?: Record, +) { + // Something has gone wrong. Bail. + if (customEventName.trim() === '') { + console.warn('buildTrackingClassNames() was called with an empty customEventName.'); + + return ''; + } + + const classNames: string[] = []; + + // Format the custom event name how Plausible expects. + // "My Custom Event" --> "My+Custom+Event" + const formattedEventName = `plausible-event-name=${customEventName.replace(/\s+/g, '+')}`; + classNames.push(formattedEventName); + + // Add each custom property. Spaces here must be replaced with plus signs, too. + if (customProperties) { + for (const [key, value] of Object.entries(customProperties)) { + const formattedValue = `${value}`.replace(/\s+/g, '+'); + classNames.push(`plausible-event-${key}=${formattedValue}`); + } + } + + return classNames.join(' '); +} diff --git a/resources/js/features/games/components/GameBreadcrumbs/GameBreadcrumbs.test.tsx b/resources/js/features/games/components/GameBreadcrumbs/GameBreadcrumbs.test.tsx new file mode 100644 index 0000000000..8aac7a7fb8 --- /dev/null +++ b/resources/js/features/games/components/GameBreadcrumbs/GameBreadcrumbs.test.tsx @@ -0,0 +1,50 @@ +import { render, screen } from '@/test'; +import { createGame, createSystem } from '@/test/factories'; + +import { GameBreadcrumbs } from './GameBreadcrumbs'; + +describe('Component: GameBreadcrumbs', () => { + it('renders without crashing', () => { + // ARRANGE + const { container } = render(); + + // ASSERT + expect(container).toBeTruthy(); + }); + + it('has a link to the All Games list', () => { + // ARRANGE + render(); + + // ASSERT + const allGamesLinkEl = screen.getByRole('link', { name: /all games/i }); + expect(allGamesLinkEl).toBeVisible(); + expect(allGamesLinkEl).toHaveAttribute('href', '/gameList.php'); + }); + + it('given a system, has a link to the system games list', () => { + // ARRANGE + const system = createSystem({ name: 'Nintendo 64' }); + const game = createGame({ system }); + + render(); + + // ASSERT + const systemGamesLinkEl = screen.getByRole('link', { name: /nintendo 64/i }); + expect(systemGamesLinkEl).toBeVisible(); + expect(systemGamesLinkEl).toHaveAttribute('href', `system.game.index,${system.id}`); + }); + + it('given a game, has a link to the game page', () => { + // ARRANGE + const system = createSystem({ name: 'Nintendo 64' }); + const game = createGame({ system }); + + render(); + + // ASSERT + const gameLinkEl = screen.getByRole('link', { name: game.title }); + expect(gameLinkEl).toBeVisible(); + expect(gameLinkEl).toHaveAttribute('href', `game.show,${{ game: game.id }}`); + }); +}); diff --git a/resources/js/features/games/components/GameBreadcrumbs/GameBreadcrumbs.tsx b/resources/js/features/games/components/GameBreadcrumbs/GameBreadcrumbs.tsx new file mode 100644 index 0000000000..3062571a0b --- /dev/null +++ b/resources/js/features/games/components/GameBreadcrumbs/GameBreadcrumbs.tsx @@ -0,0 +1,60 @@ +import type { FC } from 'react'; + +import { + BaseBreadcrumb, + BaseBreadcrumbItem, + BaseBreadcrumbLink, + BaseBreadcrumbList, + BaseBreadcrumbPage, + BaseBreadcrumbSeparator, +} from '@/common/components/+vendor/BaseBreadcrumb'; + +interface GameBreadcrumbsProps { + currentPageLabel: string; + + game?: App.Platform.Data.Game; + system?: App.Platform.Data.System; +} + +export const GameBreadcrumbs: FC = ({ currentPageLabel, game, system }) => { + return ( +
+ + + + All Games + + + {system ? ( + <> + + + + + {system.name} + + + + ) : null} + + {game ? ( + <> + + + + {game.title} + + + + ) : null} + + + + + {currentPageLabel} + + + +
+ ); +}; diff --git a/resources/js/features/games/components/GameBreadcrumbs/index.ts b/resources/js/features/games/components/GameBreadcrumbs/index.ts new file mode 100644 index 0000000000..b9bde9c324 --- /dev/null +++ b/resources/js/features/games/components/GameBreadcrumbs/index.ts @@ -0,0 +1 @@ +export * from './GameBreadcrumbs'; diff --git a/resources/js/features/games/components/GameHeading/GameHeading.test.tsx b/resources/js/features/games/components/GameHeading/GameHeading.test.tsx new file mode 100644 index 0000000000..aa80ee8429 --- /dev/null +++ b/resources/js/features/games/components/GameHeading/GameHeading.test.tsx @@ -0,0 +1,38 @@ +import { render, screen } from '@/test'; +import { createGame } from '@/test/factories'; + +import { GameHeading } from './GameHeading'; + +describe('Component: GameHeading', () => { + it('renders without crashing', () => { + // ARRANGE + const { container } = render(Hello, World); + + // ASSERT + expect(container).toBeTruthy(); + }); + + it('displays a clickable avatar of the given game', () => { + // ARRANGE + const game = createGame(); + + render(Hello, World); + + // ASSERT + const linkEl = screen.getByRole('link'); + expect(linkEl).toBeVisible(); + expect(linkEl).toHaveAttribute('href', `game.show,${{ game: game.id }}`); + + expect(screen.getByRole('img', { name: game.title })).toBeVisible(); + }); + + it('displays an accessible header from `children`', () => { + // ARRANGE + const game = createGame(); + + render(Hello, World); + + // ASSERT + expect(screen.getByRole('heading', { name: /hello, world/i })).toBeVisible(); + }); +}); diff --git a/resources/js/features/games/components/GameHeading/GameHeading.tsx b/resources/js/features/games/components/GameHeading/GameHeading.tsx new file mode 100644 index 0000000000..5d21f438a9 --- /dev/null +++ b/resources/js/features/games/components/GameHeading/GameHeading.tsx @@ -0,0 +1,20 @@ +import type { FC, ReactNode } from 'react'; + +import { GameAvatar } from '@/common/components/GameAvatar'; + +interface GameHeadingProps { + children: ReactNode; + game: App.Platform.Data.Game; +} + +export const GameHeading: FC = ({ children, game }) => { + return ( +
+
+ +
+ +

{children}

+
+ ); +}; diff --git a/resources/js/features/games/components/GameHeading/index.ts b/resources/js/features/games/components/GameHeading/index.ts new file mode 100644 index 0000000000..9c378538df --- /dev/null +++ b/resources/js/features/games/components/GameHeading/index.ts @@ -0,0 +1 @@ +export * from './GameHeading'; diff --git a/resources/js/features/games/components/HashesMainRoot/HashesList/HashesList.test.tsx b/resources/js/features/games/components/HashesMainRoot/HashesList/HashesList.test.tsx new file mode 100644 index 0000000000..814851f289 --- /dev/null +++ b/resources/js/features/games/components/HashesMainRoot/HashesList/HashesList.test.tsx @@ -0,0 +1,78 @@ +import { faker } from '@faker-js/faker'; + +import { render, screen } from '@/test'; +import { createGameHash } from '@/test/factories'; + +import { HashesList, hashesListContainerTestId } from './HashesList'; + +describe('Component: HashesList', () => { + it('renders without crashing', () => { + // ARRANGE + const { container } = render(, { + pageProps: { + hashes: [createGameHash()], + }, + }); + + // ASSERT + expect(container).toBeTruthy(); + }); + + it('given there are no hashes, renders nothing', () => { + // ARRANGE + render(, { + pageProps: { + hashes: [], + }, + }); + + // ASSERT + expect(screen.queryByTestId(hashesListContainerTestId)).not.toBeInTheDocument(); + }); + + it('renders both named and unnamed hashes', () => { + // ARRANGE + const hashes = [ + // Named + createGameHash({ name: faker.word.words(3) }), + createGameHash({ name: faker.word.words(3) }), + + // Unnamed + createGameHash({ name: null }), + ]; + + render(, { + pageProps: { hashes }, + }); + + // ASSERT + expect(screen.getAllByRole('listitem').length).toEqual(3); + }); + + it('displays the hash name and md5', () => { + // ARRANGE + const hash = createGameHash({ name: faker.word.words(3) }); + + render(, { + pageProps: { hashes: [hash] }, + }); + + // ASSERT + expect(screen.getByText(hash.name ?? '')).toBeVisible(); + expect(screen.getByText(hash.md5)).toBeVisible(); + }); + + it('given the hash has a patch URL, adds a link to it', () => { + // ARRANGE + const hash = createGameHash({ patchUrl: faker.internet.url() }); + + render(, { + pageProps: { hashes: [hash] }, + }); + + // ASSERT + const linkEl = screen.getByRole('link', { name: /download patch file/i }); + expect(linkEl).toBeVisible(); + expect(linkEl).toHaveAttribute('href', hash.patchUrl); + }); +}); diff --git a/resources/js/features/games/components/HashesMainRoot/HashesList/HashesList.tsx b/resources/js/features/games/components/HashesMainRoot/HashesList/HashesList.tsx new file mode 100644 index 0000000000..040951bcf1 --- /dev/null +++ b/resources/js/features/games/components/HashesMainRoot/HashesList/HashesList.tsx @@ -0,0 +1,43 @@ +import { usePage } from '@inertiajs/react'; +import type { FC } from 'react'; + +import { Embed } from '@/common/components/Embed/Embed'; + +import { HashesListItem } from './HashesListItem'; + +export const hashesListContainerTestId = 'hashes-list'; + +export const HashesList: FC = () => { + const { + props: { hashes }, + } = usePage(); + + if (!hashes.length) { + return null; + } + + const namedHashes = hashes.filter((hash) => !!hash.name?.trim()); + const unnamedHashes = hashes.filter((hash) => !hash.name?.trim()); + + return ( + + {namedHashes.length ? ( +
    + {namedHashes.map((labeledHash) => ( + + ))} +
+ ) : null} + + {namedHashes.length && unnamedHashes.length ?
: null} + + {unnamedHashes.length ? ( +
    + {unnamedHashes.map((unlabeledHash) => ( + + ))} +
+ ) : null} + + ); +}; diff --git a/resources/js/features/games/components/HashesMainRoot/HashesList/HashesListItem.tsx b/resources/js/features/games/components/HashesMainRoot/HashesList/HashesListItem.tsx new file mode 100644 index 0000000000..5722526ca8 --- /dev/null +++ b/resources/js/features/games/components/HashesMainRoot/HashesList/HashesListItem.tsx @@ -0,0 +1,52 @@ +import type { FC } from 'react'; + +import { buildTrackingClassNames } from '@/common/utils/buildTrackingClassNames'; + +interface HashListingProps { + hash: App.Platform.Data.GameHash; +} + +export const HashesListItem: FC = ({ hash }) => { + return ( +
  • +

    + {hash.name ? {hash.name} : null} + + {hash.labels.length ? ( + <> + {hash.labels.map((hashLabel) => ( + + ))} + + ) : null} +

    + +
    +

    {hash.md5}

    + + {hash.patchUrl ? ( + + Download Patch File + + ) : null} +
    +
  • + ); +}; + +interface HashLabelProps { + hashLabel: App.Platform.Data.GameHashLabel; +} + +export const HashLabel: FC = ({ hashLabel }) => { + const { imgSrc, label } = hashLabel; + + if (!imgSrc) { + return [{label}]; + } + + return ; +}; diff --git a/resources/js/features/games/components/HashesMainRoot/HashesList/index.ts b/resources/js/features/games/components/HashesMainRoot/HashesList/index.ts new file mode 100644 index 0000000000..05ef9e589e --- /dev/null +++ b/resources/js/features/games/components/HashesMainRoot/HashesList/index.ts @@ -0,0 +1 @@ +export * from './HashesList'; diff --git a/resources/js/features/games/components/HashesMainRoot/HashesMainRoot.test.tsx b/resources/js/features/games/components/HashesMainRoot/HashesMainRoot.test.tsx new file mode 100644 index 0000000000..9a3bb322f0 --- /dev/null +++ b/resources/js/features/games/components/HashesMainRoot/HashesMainRoot.test.tsx @@ -0,0 +1,34 @@ +import { render, screen } from '@/test'; +import { createGame } from '@/test/factories'; + +import { HashesMainRoot } from './HashesMainRoot'; + +describe('Component: HashesMainRoot', () => { + it('renders without crashing', () => { + // ARRANGE + const { container } = render(, { + pageProps: { + can: { manageGameHashes: false }, + game: createGame(), + hashes: [], + }, + }); + + // ASSERT + expect(container).toBeTruthy(); + }); + + it('given the user can manage hashes, shows a manage link', () => { + // ARRANGE + render(, { + pageProps: { + can: { manageGameHashes: true }, + game: createGame(), + hashes: [], + }, + }); + + // ASSERT + expect(screen.getByRole('link', { name: /manage hashes/i })).toBeVisible(); + }); +}); diff --git a/resources/js/features/games/components/HashesMainRoot/HashesMainRoot.tsx b/resources/js/features/games/components/HashesMainRoot/HashesMainRoot.tsx new file mode 100644 index 0000000000..d5a150aeea --- /dev/null +++ b/resources/js/features/games/components/HashesMainRoot/HashesMainRoot.tsx @@ -0,0 +1,74 @@ +import { usePage } from '@inertiajs/react'; +import type { FC } from 'react'; +import { LuSave } from 'react-icons/lu'; + +import { baseButtonVariants } from '@/common/components/+vendor/BaseButton'; +import { Embed } from '@/common/components/Embed/Embed'; + +import { GameBreadcrumbs } from '../GameBreadcrumbs'; +import { GameHeading } from '../GameHeading/GameHeading'; +import { HashesList } from './HashesList'; + +export const HashesMainRoot: FC = () => { + const { + props: { can, game, hashes }, + } = usePage(); + + return ( +
    + + Supported Game Files + +
    + {can.manageGameHashes ? ( + + + Manage Hashes + + ) : null} + + +

    + This page shows you what ROM hashes are compatible with this game's achievements. +

    + +

    + {game.forumTopicId ? ( + <> + Additional information for these hashes may be listed on{' '} + + the game's official forum topic + + . + + ) : null}{' '} + Details on how the hash is generated for each system can be found{' '} + + here + + .{' '} +

    + + +
    +

    + There {hashes.length === 1 ? 'is' : 'are'} currently{' '} + {hashes.length} supported game file{' '} + {hashes.length === 1 ? 'hash' : 'hashes'} registered for this game. +

    + + +
    +
    +
    + ); +}; diff --git a/resources/js/features/games/components/HashesMainRoot/index.ts b/resources/js/features/games/components/HashesMainRoot/index.ts new file mode 100644 index 0000000000..da529098fd --- /dev/null +++ b/resources/js/features/games/components/HashesMainRoot/index.ts @@ -0,0 +1 @@ +export * from './HashesMainRoot'; diff --git a/resources/js/pages/game/[game]/hashes.tsx b/resources/js/pages/game/[game]/hashes.tsx new file mode 100644 index 0000000000..7d70a674c5 --- /dev/null +++ b/resources/js/pages/game/[game]/hashes.tsx @@ -0,0 +1,26 @@ +import { Head } from '@inertiajs/react'; + +import { AppLayout } from '@/common/layouts/AppLayout'; +import type { AppPage } from '@/common/models'; +import { HashesMainRoot } from '@/features/games/components/HashesMainRoot'; + +const Hashes: AppPage = ({ game, hashes }) => { + return ( + <> + + + + + + + + + ); +}; + +Hashes.layout = (page) => {page}; + +export default Hashes; diff --git a/resources/js/ssr.tsx b/resources/js/ssr.tsx index 3fa495f139..83b1bdc76c 100644 --- a/resources/js/ssr.tsx +++ b/resources/js/ssr.tsx @@ -7,7 +7,7 @@ import ReactDOMServer from 'react-dom/server'; import type { RouteName, RouteParams } from 'ziggy-js'; import { route } from '../../vendor/tightenco/ziggy'; -import { AppProviders } from './common/components/AppProviders/AppProviders'; +import { AppProviders } from './common/components/AppProviders'; const appName = import.meta.env.APP_NAME || 'RetroAchievements'; @@ -32,7 +32,7 @@ createServer((page) => }); return ( - + ); diff --git a/resources/js/test/factories/createGame.ts b/resources/js/test/factories/createGame.ts new file mode 100644 index 0000000000..921e698398 --- /dev/null +++ b/resources/js/test/factories/createGame.ts @@ -0,0 +1,12 @@ +import { createFactory } from '../createFactory'; +import { createSystem } from './createSystem'; + +export const createGame = createFactory((faker) => { + return { + id: faker.number.int({ min: 1, max: 99999 }), + title: faker.word.words(3), + badgeUrl: faker.internet.url(), + forumTopicId: faker.number.int({ min: 1, max: 99999 }), + system: createSystem(), + }; +}); diff --git a/resources/js/test/factories/createGameHash.ts b/resources/js/test/factories/createGameHash.ts new file mode 100644 index 0000000000..dedba16e34 --- /dev/null +++ b/resources/js/test/factories/createGameHash.ts @@ -0,0 +1,19 @@ +import { createFactory } from '../createFactory'; +import { createGameHashLabel } from './createGameHashLabel'; + +export const createGameHash = createFactory((faker) => { + const labelsCount = faker.number.int({ min: 0, max: 2 }); + + const labels: App.Platform.Data.GameHashLabel[] = []; + for (let i = 0; i < labelsCount; i += 1) { + labels.push(createGameHashLabel()); + } + + return { + labels, + id: faker.number.int({ min: 1, max: 99999 }), + md5: faker.string.alphanumeric(32), + name: faker.word.words(3), + patchUrl: faker.internet.url(), + }; +}); diff --git a/resources/js/test/factories/createGameHashLabel.ts b/resources/js/test/factories/createGameHashLabel.ts new file mode 100644 index 0000000000..88239323ad --- /dev/null +++ b/resources/js/test/factories/createGameHashLabel.ts @@ -0,0 +1,17 @@ +import { createFactory } from '../createFactory'; + +export const createGameHashLabel = createFactory((faker) => ({ + imgSrc: faker.internet.url(), + label: faker.helpers.arrayElement([ + 'nointro', + 'rapatches', + 'fbneo', + 'goodtools', + 'redump', + 'mamesl', + 'tosec', + 'itchio', + 'msu1', + 'lostlevel', + ]), +})); diff --git a/resources/js/test/factories/createSystem.ts b/resources/js/test/factories/createSystem.ts new file mode 100644 index 0000000000..849d15477e --- /dev/null +++ b/resources/js/test/factories/createSystem.ts @@ -0,0 +1,10 @@ +import { createFactory } from '../createFactory'; + +export const createSystem = createFactory((faker) => { + return { + id: faker.number.int({ min: 1, max: 150 }), + name: faker.word.words(2), + nameFull: faker.word.words(4), + nameShort: faker.string.alphanumeric(3), + }; +}); diff --git a/resources/js/test/factories/index.ts b/resources/js/test/factories/index.ts index 97c8eec043..d59d275bd8 100644 --- a/resources/js/test/factories/index.ts +++ b/resources/js/test/factories/index.ts @@ -1 +1,6 @@ +export * from './createForumTopicComment'; +export * from './createGame'; +export * from './createGameHash'; +export * from './createGameHashLabel'; +export * from './createSystem'; export * from './createUser'; diff --git a/resources/js/test/setup.tsx b/resources/js/test/setup.tsx index 0e27c34efc..bf3e0a35bc 100644 --- a/resources/js/test/setup.tsx +++ b/resources/js/test/setup.tsx @@ -5,6 +5,7 @@ import { render as defaultRender } from '@testing-library/react'; import type { ReactNode } from 'react'; import { AppProviders } from '@/common/components/AppProviders'; +import type { AppGlobalProps } from '@/common/models'; export * from '@testing-library/react'; @@ -106,14 +107,23 @@ vi.mock('laravel-react-i18n', () => ({ type DefaultParams = Parameters; type RenderUI = DefaultParams[0]; -type RenderOptions = DefaultParams[1] & { pageProps?: Record }; // augment this as necessary +type RenderOptions> = DefaultParams[1] & { + pageProps?: Partial; +}; interface WrapperProps { children: ReactNode; } -export function render(ui: RenderUI, { wrapper, pageProps = {}, ...options }: RenderOptions = {}) { - vi.spyOn(InertiajsReactModule, 'usePage').mockImplementationOnce(() => ({ +export function render>( + ui: RenderUI, + { + wrapper, + pageProps = {} as Partial, + ...options + }: RenderOptions = {}, +) { + vi.spyOn(InertiajsReactModule, 'usePage').mockImplementation(() => ({ component: '', props: pageProps as any, rememberedState: {}, diff --git a/resources/js/types/generated.d.ts b/resources/js/types/generated.d.ts index e4dd0b8264..23dd8a5a1d 100644 --- a/resources/js/types/generated.d.ts +++ b/resources/js/types/generated.d.ts @@ -12,12 +12,12 @@ declare namespace App.Data { id: number; title: string; createdAt: string; - user: App.Data.User | null; latestComment?: App.Data.ForumTopicComment; commentCount24h?: number; oldestComment24hId?: number; commentCount7d?: number; oldestComment7dId?: number; + user: App.Data.User | null; }; export type __UNSAFE_PaginatedData = { currentPage: number; @@ -42,6 +42,9 @@ declare namespace App.Data { roles?: App.Models.UserRole[]; unreadMessageCount?: number | null; }; + export type UserPermissions = { + manageGameHashes?: boolean; + }; } declare namespace App.Models { export type UserRole = @@ -70,6 +73,29 @@ declare namespace App.Models { | 'developer-veteran'; } declare namespace App.Platform.Data { + export type Game = { + id: number; + title: string; + badgeUrl?: string; + forumTopicId?: number; + system?: App.Platform.Data.System; + }; + export type GameHash = { + id: number; + md5: string; + name: string | null; + labels: Array; + patchUrl: string | null; + }; + export type GameHashLabel = { + label: string; + imgSrc: string | null; + }; + export type GameHashesPageProps = { + game: App.Platform.Data.Game; + hashes: Array; + can: App.Data.UserPermissions; + }; export type PlayerResettableGameAchievement = { id: number; title: string; @@ -83,7 +109,14 @@ declare namespace App.Platform.Data { numAwarded: number; numPossible: number; }; + export type System = { + id: number; + name: string; + nameFull?: string; + nameShort?: string; + }; } declare namespace App.Platform.Enums { + export type GameSetType = 'hub' | 'similar-games'; export type AchievementFlag = 3 | 5; } diff --git a/resources/js/ziggy.d.ts b/resources/js/ziggy.d.ts index 4ad6a4de79..4b59077da1 100644 --- a/resources/js/ziggy.d.ts +++ b/resources/js/ziggy.d.ts @@ -159,13 +159,6 @@ declare module 'ziggy-js' { } ], "game.random": [], - "game.hash": [ - { - "name": "game", - "required": true, - "binding": "ID" - } - ], "game.hash.manage": [ { "name": "game", @@ -263,6 +256,13 @@ declare module 'ziggy-js' { "claims.expiring": [], "claims.completed": [], "claims.active": [], + "game.hashes.index": [ + { + "name": "game", + "required": true, + "binding": "ID" + } + ], "game-hash.update": [ { "name": "gameHash", diff --git a/resources/views/components/game/link-buttons/index.blade.php b/resources/views/components/game/link-buttons/index.blade.php index f9c5c9cc1c..b2267c43d7 100644 --- a/resources/views/components/game/link-buttons/index.blade.php +++ b/resources/views/components/game/link-buttons/index.blade.php @@ -67,7 +67,7 @@ @can('viewAny', App\Models\GameHash::class) Supported Game Files diff --git a/resources/views/components/supported-game-files/hash-listing.blade.php b/resources/views/components/supported-game-files/hash-listing.blade.php deleted file mode 100644 index 99a0dbfe6f..0000000000 --- a/resources/views/components/supported-game-files/hash-listing.blade.php +++ /dev/null @@ -1,40 +0,0 @@ -@props([ - 'hash' => null, // GameHash -]) - -
  • -

    - @if ($hash->name) - {{ $hash->name }} - @endif - - @if (!empty($hash->labels)) - @foreach (explode(',', $hash->labels) as $label) - @if (empty($label)) - @continue; - @endif - - @php - $image = "/assets/images/labels/" . $label . '.png'; - $publicPath = public_path($image); - @endphp - - @if (file_exists($publicPath)) - - @else - [{{ $label }}] - @endif - @endforeach - @endif -

    - -
    -

    - {{ $hash->md5 }} -

    - - @if ($hash->patch_url) - Download Patch File - @endif -
    -
  • diff --git a/resources/views/pages/game/[game]/hashes/index.blade.php b/resources/views/pages/game/[game]/hashes/index.blade.php deleted file mode 100644 index d1fbe5155c..0000000000 --- a/resources/views/pages/game/[game]/hashes/index.blade.php +++ /dev/null @@ -1,105 +0,0 @@ -hashes()->with('user')->orderBy('name')->orderBy('md5')->get(); - $numHashes = $hashes->count(); - - $unlabeledHashes = $hashes->filter(function ($hash) { - return empty($hash->name); - }); - $labeledHashes = $hashes->reject(function ($hash) { - return empty($hash->name); - }); - - return $view->with([ - 'labeledHashes' => $labeledHashes, - 'numHashes' => $numHashes, - 'unlabeledHashes' => $unlabeledHashes, - ]); -}); - -?> - -@props([ - 'labeledHashes' => null, // Collection - 'numHashes' => 0, - 'unlabeledHashes' => null, // Collection -]) - - - - -
    - {!! gameAvatar($game->toArray(), label: false, iconSize: 48, iconClass: 'rounded-sm') !!} -

    Supported Game Files

    -
    - - @can('manage', App\Models\GameHash::class, ['game' => $game]) -
    - -
    - @endcan - -
    -

    - - RetroAchievements requires that the game files you use with your emulator - are the same as, or compatible with, those used to create the game's achievements. - - This page shows you what ROM hashes are compatible with the game's achievements. -

    - -

    - Details on how the hash is generated for each system can be found - here. - - @if ($game->ForumTopicID > 0) - Additional information for these hashes may be listed on the - official forum topic. - @endif -

    -
    - -

    - There {{ $numHashes === 1 ? 'is' : 'are' }} currently {{ $numHashes }} - supported game file {{ strtolower(__res('game-hash', $numHashes)) }} registered for this game. -

    - -
    -
      - @foreach ($labeledHashes as $hash) - - @endforeach -
    - - @if (!$labeledHashes->isEmpty() && !$unlabeledHashes->isEmpty()) -
    - @endif - - @if (!$unlabeledHashes->isEmpty()) -

    Unlabeled Game File Hashes

    - -
      - @foreach ($unlabeledHashes as $hash) - - @endforeach -
    - @endif -
    -
    From 1f60dd0cbe91b6d199e060fc62ebe53c4ced3670 Mon Sep 17 00:00:00 2001 From: Wes Copeland Date: Tue, 3 Sep 2024 18:05:28 -0400 Subject: [PATCH 5/7] revert laravel/react i18n changes (#2673) --- .gitignore | 1 - lang/en_US.json | 12 --- package-lock.json | 30 ------- package.json | 1 - resources/js/app.tsx | 15 +--- .../AppProviders/AppProviders.test.tsx | 13 --- .../components/AppProviders/AppProviders.tsx | 18 ---- .../common/components/AppProviders/index.ts | 1 - .../AggregateRecentPostLinks.tsx | 7 +- .../RecentPostsBreadcrumbs.tsx | 7 +- .../RecentPostsCards/RecentPostsCards.tsx | 5 +- .../RecentPostsMainRoot.tsx | 5 +- .../RecentPostsPagination.tsx | 9 +- .../RecentPostsTable/RecentPostsTable.tsx | 9 +- resources/js/ssr.tsx | 7 +- resources/js/test/setup.tsx | 83 +------------------ vite.config.ts | 2 - 17 files changed, 15 insertions(+), 210 deletions(-) delete mode 100644 lang/en_US.json delete mode 100644 resources/js/common/components/AppProviders/AppProviders.test.tsx delete mode 100644 resources/js/common/components/AppProviders/AppProviders.tsx delete mode 100644 resources/js/common/components/AppProviders/index.ts diff --git a/.gitignore b/.gitignore index 0551a32370..b0ba9275d0 100644 --- a/.gitignore +++ b/.gitignore @@ -49,4 +49,3 @@ yarn-error.log .phpunit.result.cache _ide_helper.php _ide_helper_models.php -lang/php_*.json \ No newline at end of file diff --git a/lang/en_US.json b/lang/en_US.json deleted file mode 100644 index dc60506839..0000000000 --- a/lang/en_US.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - ":count posts in the last 24 hours": ":count posts in the last 24 hours", - ":count posts in the last 7 days": ":count posts in the last 7 days", - "Additional Posts": "Additional Posts", - "Forum Index": "Forum Index", - "in": "in", - "Last Post By": "Last Post By", - "Message": "Message", - "Next :count": "Next :count", - "Previous :count": "Previous :count", - "Recent Posts": "Recent Posts" -} diff --git a/package-lock.json b/package-lock.json index b0609a2f8d..752e3cdb2c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,6 @@ "clsx": "^2.1.1", "dayjs": "^1.11.11", "js-cookie": "^3.0.5", - "laravel-react-i18n": "^2.0.4", "linkify-html": "^4.1.3", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -8096,20 +8095,6 @@ "node": ">=10" } }, - "node_modules/laravel-react-i18n": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/laravel-react-i18n/-/laravel-react-i18n-2.0.4.tgz", - "integrity": "sha512-gMQVUK9mjM4SXhuiXg5rLNIUslnblB3v3anqzT+/NzodRFnSL6nWDTjpo0Sc8G+WJ1WZxmLLLgaoHXSTk2ZLUQ==", - "license": "MIT", - "dependencies": { - "php-array-reader": "^1.3.5", - "react": "^18.2.0" - }, - "engines": { - "node": ">=16.0.0", - "npm": ">=9.0.0" - } - }, "node_modules/laravel-vite-plugin": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-1.0.4.tgz", @@ -8823,21 +8808,6 @@ "node": ">= 14.16" } }, - "node_modules/php-array-reader": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/php-array-reader/-/php-array-reader-1.3.5.tgz", - "integrity": "sha512-jnFhTzSDBHmTJFGmhrMG9vZ+mKBkPm8egcsshW3eSdEyoqthVDUStc3w85aDD4InK0h4LoqP0nd3/xGad2zapA==", - "license": "MIT", - "dependencies": { - "php-parser": "^3.0.3" - } - }, - "node_modules/php-parser": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/php-parser/-/php-parser-3.1.5.tgz", - "integrity": "sha512-jEY2DcbgCm5aclzBdfW86GM6VEIWcSlhTBSHN1qhJguVePlYe28GhwS0yoeLYXpM2K8y6wzLwrbq814n2PHSoQ==", - "license": "BSD-3-Clause" - }, "node_modules/picocolors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", diff --git a/package.json b/package.json index d1390d36e3..a51eccc656 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,6 @@ "clsx": "^2.1.1", "dayjs": "^1.11.11", "js-cookie": "^3.0.5", - "laravel-react-i18n": "^2.0.4", "linkify-html": "^4.1.3", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/resources/js/app.tsx b/resources/js/app.tsx index 0666bf4598..13d534454d 100644 --- a/resources/js/app.tsx +++ b/resources/js/app.tsx @@ -2,8 +2,6 @@ import { createInertiaApp } from '@inertiajs/react'; import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'; import { createRoot, hydrateRoot } from 'react-dom/client'; -import { AppProviders } from './common/components/AppProviders'; - const appName = import.meta.env.APP_NAME || 'RetroAchievements'; createInertiaApp({ @@ -14,21 +12,12 @@ createInertiaApp({ setup({ el, App, props }) { if (import.meta.env.DEV) { - createRoot(el).render( - - - , - ); + createRoot(el).render(); return; } - hydrateRoot( - el, - - - , - ); + hydrateRoot(el, ); }, progress: { diff --git a/resources/js/common/components/AppProviders/AppProviders.test.tsx b/resources/js/common/components/AppProviders/AppProviders.test.tsx deleted file mode 100644 index c82dadbf43..0000000000 --- a/resources/js/common/components/AppProviders/AppProviders.test.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { render } from '@/test'; - -import { AppProviders } from './AppProviders'; - -describe('Component: AppProviders', () => { - it('renders without crashing', () => { - // ARRANGE - const { container } = render(content, { wrapper: () => <> }); - - // ASSERT - expect(container).toBeTruthy(); - }); -}); diff --git a/resources/js/common/components/AppProviders/AppProviders.tsx b/resources/js/common/components/AppProviders/AppProviders.tsx deleted file mode 100644 index 8edadd06fe..0000000000 --- a/resources/js/common/components/AppProviders/AppProviders.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { LaravelReactI18nProvider } from 'laravel-react-i18n'; -import type { FC, ReactNode } from 'react'; - -interface AppProvidersProps { - children: ReactNode; -} - -export const AppProviders: FC = ({ children }) => { - return ( - - {children} - - ); -}; diff --git a/resources/js/common/components/AppProviders/index.ts b/resources/js/common/components/AppProviders/index.ts deleted file mode 100644 index 4dfaf720df..0000000000 --- a/resources/js/common/components/AppProviders/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './AppProviders'; diff --git a/resources/js/features/forums/components/RecentPostsMainRoot/AggregateRecentPostLinks/AggregateRecentPostLinks.tsx b/resources/js/features/forums/components/RecentPostsMainRoot/AggregateRecentPostLinks/AggregateRecentPostLinks.tsx index 55a151c39b..f960e4930b 100644 --- a/resources/js/features/forums/components/RecentPostsMainRoot/AggregateRecentPostLinks/AggregateRecentPostLinks.tsx +++ b/resources/js/features/forums/components/RecentPostsMainRoot/AggregateRecentPostLinks/AggregateRecentPostLinks.tsx @@ -1,4 +1,3 @@ -import { useLaravelReactI18n } from 'laravel-react-i18n'; import type { FC } from 'react'; import type { RecentActiveForumTopic } from '@/features/forums/models'; @@ -8,8 +7,6 @@ interface AggregateRecentPostLinksProps { } export const AggregateRecentPostLinks: FC = ({ topic }) => { - const { t } = useLaravelReactI18n(); - const { commentCount24h, commentCount7d, oldestComment24hId, oldestComment7dId, id } = topic; if (!commentCount7d || commentCount7d <= 1) { @@ -24,13 +21,13 @@ export const AggregateRecentPostLinks: FC = ({ to diff --git a/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsBreadcrumbs/RecentPostsBreadcrumbs.tsx b/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsBreadcrumbs/RecentPostsBreadcrumbs.tsx index 0c3788cd09..276a1485db 100644 --- a/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsBreadcrumbs/RecentPostsBreadcrumbs.tsx +++ b/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsBreadcrumbs/RecentPostsBreadcrumbs.tsx @@ -1,4 +1,3 @@ -import { useLaravelReactI18n } from 'laravel-react-i18n'; import type { FC } from 'react'; import { @@ -11,20 +10,18 @@ import { } from '@/common/components/+vendor/BaseBreadcrumb'; export const RecentPostsBreadcrumbs: FC = () => { - const { t } = useLaravelReactI18n(); - return (
    - {t('Forum Index')} + Forum Index - {t('Recent Posts')} + Recent Posts diff --git a/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsCards/RecentPostsCards.tsx b/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsCards/RecentPostsCards.tsx index bc8d69b6d4..378540d4fe 100644 --- a/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsCards/RecentPostsCards.tsx +++ b/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsCards/RecentPostsCards.tsx @@ -1,5 +1,4 @@ import { usePage } from '@inertiajs/react'; -import { useLaravelReactI18n } from 'laravel-react-i18n'; import type { FC } from 'react'; import { UserAvatar } from '@/common/components/UserAvatar'; @@ -9,8 +8,6 @@ import { AggregateRecentPostLinks } from '../AggregateRecentPostLinks'; import { PostTimestamp } from '../PostTimestamp'; export const RecentPostsCards: FC = () => { - const { t } = useLaravelReactI18n(); - const { props } = usePage(); const { auth, paginatedTopics } = props; @@ -36,7 +33,7 @@ export const RecentPostsCards: FC = () => {

    - {t('in')}{' '} + in{' '} diff --git a/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsMainRoot.tsx b/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsMainRoot.tsx index 219e5d8d69..c666ed9548 100644 --- a/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsMainRoot.tsx +++ b/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsMainRoot.tsx @@ -1,4 +1,3 @@ -import { useLaravelReactI18n } from 'laravel-react-i18n'; import type { FC } from 'react'; import { RecentPostsBreadcrumbs } from './RecentPostsBreadcrumbs'; @@ -7,13 +6,11 @@ import { RecentPostsPagination } from './RecentPostsPagination'; import { RecentPostsTable } from './RecentPostsTable'; export const RecentPostsMainRoot: FC = () => { - const { t } = useLaravelReactI18n(); - return (

    -

    {t('Recent Posts')}

    +

    Recent Posts

    diff --git a/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsPagination/RecentPostsPagination.tsx b/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsPagination/RecentPostsPagination.tsx index dd79510a2a..d7b341dbc7 100644 --- a/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsPagination/RecentPostsPagination.tsx +++ b/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsPagination/RecentPostsPagination.tsx @@ -1,5 +1,4 @@ import { usePage } from '@inertiajs/react'; -import { useLaravelReactI18n } from 'laravel-react-i18n'; import type { FC } from 'react'; import { @@ -12,8 +11,6 @@ import { import type { RecentPostsPageProps } from '@/features/forums/models'; export const RecentPostsPagination: FC = () => { - const { t } = useLaravelReactI18n(); - const { paginatedTopics } = usePage().props; const { @@ -31,16 +28,14 @@ export const RecentPostsPagination: FC = () => { {previousPageUrl ? ( - {t('Previous :count', { count: perPage })} + Previous {perPage} ) : null} {nextPageUrl ? ( - - {t('Next :count', { count: perPage })} - + Next {perPage} ) : null} diff --git a/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsTable/RecentPostsTable.tsx b/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsTable/RecentPostsTable.tsx index afb52f14a5..eb039a3ed9 100644 --- a/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsTable/RecentPostsTable.tsx +++ b/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsTable/RecentPostsTable.tsx @@ -1,5 +1,4 @@ import { usePage } from '@inertiajs/react'; -import { useLaravelReactI18n } from 'laravel-react-i18n'; import type { FC } from 'react'; import { UserAvatar } from '@/common/components/UserAvatar'; @@ -9,8 +8,6 @@ import { AggregateRecentPostLinks } from '../AggregateRecentPostLinks'; import { PostTimestamp } from '../PostTimestamp'; export const RecentPostsTable: FC = () => { - const { t } = useLaravelReactI18n(); - const { props } = usePage(); const { auth, paginatedTopics } = props; @@ -19,9 +16,9 @@ export const RecentPostsTable: FC = () => {
    Last Post ByMessageAdditional Posts{t('Last Post By')}{t('Message')}{t('Additional Posts')}
    - - - + + + diff --git a/resources/js/ssr.tsx b/resources/js/ssr.tsx index 83b1bdc76c..d245677e70 100644 --- a/resources/js/ssr.tsx +++ b/resources/js/ssr.tsx @@ -7,7 +7,6 @@ import ReactDOMServer from 'react-dom/server'; import type { RouteName, RouteParams } from 'ziggy-js'; import { route } from '../../vendor/tightenco/ziggy'; -import { AppProviders } from './common/components/AppProviders'; const appName = import.meta.env.APP_NAME || 'RetroAchievements'; @@ -31,11 +30,7 @@ createServer((page) => location: new URL(page.props.ziggy.location), }); - return ( - - - - ); + return ; }, }), ); diff --git a/resources/js/test/setup.tsx b/resources/js/test/setup.tsx index bf3e0a35bc..28273e5d7b 100644 --- a/resources/js/test/setup.tsx +++ b/resources/js/test/setup.tsx @@ -4,7 +4,6 @@ import * as InertiajsReactModule from '@inertiajs/react'; import { render as defaultRender } from '@testing-library/react'; import type { ReactNode } from 'react'; -import { AppProviders } from '@/common/components/AppProviders'; import type { AppGlobalProps } from '@/common/models'; export * from '@testing-library/react'; @@ -14,86 +13,6 @@ vi.mock('@inertiajs/react', () => ({ usePage: vi.fn(), })); -/** - * laravel-react-i18n does not support Vitest, and without any intervention - * it will believe it's being rendered in the browser and cause React to - * output no DOM. Rather than trying to hack laravel-react-i18n to fix this, - * it's better to just mock the functions we need to test basic UI functionality. - */ -vi.mock('laravel-react-i18n', () => ({ - __esModule: true, - - LaravelReactI18nProvider: ({ children }: any) => <>{children}, - - useLaravelReactI18n: () => ({ - loading: false, - currentLocale: () => 'en_US', - - /** - * t('Welcome!'); // "Welcome" - * t('Welcome, :name!', { name: 'Francisco' }); // "Welcome, Francisco!" - * t(':count apples', { count: 2 }); // "2 apples" - */ - t: (key: string, replacements: Record) => { - return key.replace(/:([a-zA-Z]+)/g, (_, match) => { - const replacementKey = Object.keys(replacements).find( - (key) => key.toLowerCase() === match.toLowerCase(), - ); - - const replacement = replacementKey ? replacements[replacementKey] : match; - - return replacement !== undefined ? String(replacement) : match; - }); - }, - - /** - * tChoice('{0} There are none|[1,19] There are some|[20,*] There are many', 0); // "There are none" - * tChoice('{0} There are none|[1,19] There are some|[20,*] There are many', 4); // "There are some" - * tChoice('{0} There are none|[1,19] There are some|[20,*] There are many', 20); // "There are many" - * tChoice('There is one apple|There are many apples', 1); // "There is one apple" - * tChoice('There is one apple|There are many apples', 4); // "There are many apples" - * tChoice('{1} :count minute ago|[2,*] :count minutes ago', 1); // "1 minute ago" - * tChoice('{1} :count minute ago|[2,*] :count minutes ago', 4); // "4 minutes ago" - */ - tChoice: (key: string, count: number, replacements: Record = {}) => { - const choices = key.split('|'); - let selectedChoice = choices[choices.length - 1]; // Default to the last choice (plural) - - for (let i = 0; i < choices.length; i++) { - const match = choices[i].match(/^\{(\d+)\}\s|^\[(\d+),(\*|\d+)\]\s/); - if (match) { - const exact = match[1] !== undefined ? parseInt(match[1]) : undefined; - const rangeStart = match[2] !== undefined ? parseInt(match[2]) : undefined; - const rangeEnd = - match[3] === '*' ? Infinity : match[3] !== undefined ? parseInt(match[3]) : undefined; - - if (exact !== undefined && count === exact) { - selectedChoice = choices[i].replace(match[0], '').trim(); - break; - } else if ( - rangeEnd && - rangeStart !== undefined && - count >= rangeStart && - count <= rangeEnd - ) { - selectedChoice = choices[i].replace(match[0], '').trim(); - break; - } - } else if (count === 1 && choices.length === 2) { - selectedChoice = choices[0].trim(); - break; - } - } - - return selectedChoice.replace(/:([a-zA-Z]+)/g, (_, match) => { - const replacement = replacements[match] || count; - - return replacement; - }); - }, - }), -})); - /* |-------------------------------------------------------------------------- | Test Suite Custom Render Method @@ -133,7 +52,7 @@ export function render>( })); if (!wrapper) { - wrapper = ({ children }: WrapperProps) => {children}; + wrapper = ({ children }: WrapperProps) => <>{children}; } return { diff --git a/vite.config.ts b/vite.config.ts index ffbebb2c1d..95aba92eea 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,7 +2,6 @@ import react from '@vitejs/plugin-react'; import { existsSync, readFileSync } from 'fs'; -import i18n from 'laravel-react-i18n/vite'; import laravel from 'laravel-vite-plugin'; import { homedir } from 'os'; import { resolve } from 'path'; @@ -38,7 +37,6 @@ export default defineConfig(({ mode, isSsrBuild }) => { refresh: ['resources/views/**'], }), react(), - i18n(), ], ssr: { From 54f6bb57ad8364270e9ee4268889414aa7587eae Mon Sep 17 00:00:00 2001 From: Wes Copeland Date: Wed, 4 Sep 2024 20:49:28 -0400 Subject: [PATCH 6/7] fix(deps): bump filament to latest to fix some Laravel 11 compatibility issues (#2667) --- app/Support/Shortcode/Shortcode.php | 21 +- composer.json | 2 +- composer.lock | 350 ++++++++++++++-------------- 3 files changed, 185 insertions(+), 188 deletions(-) diff --git a/app/Support/Shortcode/Shortcode.php b/app/Support/Shortcode/Shortcode.php index 1ad30968ad..1e357dd9a7 100644 --- a/app/Support/Shortcode/Shortcode.php +++ b/app/Support/Shortcode/Shortcode.php @@ -385,7 +385,7 @@ private function autolinkRetroachievementsUrls(string $text): string ) # End negative lookahead assertion. ~ix', function ($matches) { - $subdomain = isset($matches[1]) ? $matches[1] : ''; + $subdomain = $matches[1]; $path = isset($matches[2]) ? '/' . $matches[2] : ''; return 'https://' . $subdomain . 'retroachievements.org' . $path . ''; @@ -457,19 +457,16 @@ function ($matches) { $videoId = $matches[1]; $query = []; - // Are there additional query parameters in the URL? - if (isset($matches[2])) { - // Parse the query parameters and populate them into $query. - parse_str(ltrim($matches[2], '?'), $query); + // Parse the query parameters and populate them into $query. + parse_str(ltrim($matches[2], '?'), $query); - // Check if the "t" parameter (timestamp) is present. - if (isset($query['t'])) { - // "t" has to be converted to a time compatible with youtube-nocookie.com embeds. - $query['start'] = $this->convertYouTubeTime($query['t']); + // Check if the "t" parameter (timestamp) is present. + if (isset($query['t'])) { + // "t" has to be converted to a time compatible with youtube-nocookie.com embeds. + $query['start'] = $this->convertYouTubeTime($query['t']); - // Once converted, remove the "t" parameter so we don't accidentally duplicate it. - unset($query['t']); - } + // Once converted, remove the "t" parameter so we don't accidentally duplicate it. + unset($query['t']); } $query = http_build_query($query); diff --git a/composer.json b/composer.json index 932c6f21cb..554baafa53 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,7 @@ "aws/aws-sdk-php": "^3.235", "doctrine/dbal": "^3.4", "fico7489/laravel-pivot": "^3.0", - "filament/filament": "^3.2.72", + "filament/filament": "^3.2.110", "graham-campbell/markdown": "^15.0", "guzzlehttp/guzzle": "^7.5", "inertiajs/inertia-laravel": "^1.3", diff --git a/composer.lock b/composer.lock index 424f3bb284..03ebfefa58 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "cff88df07725b424288844d54eae9f69", + "content-hash": "d8f221057e549201b7173260c432261c", "packages": [ { "name": "amphp/amp", @@ -987,16 +987,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.320.4", + "version": "3.321.2", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "e6af3e760864d43a30d8b7deb4f9dc6a49a5f66a" + "reference": "c04f8f30891cee8480c132778cd4cc486467e77a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/e6af3e760864d43a30d8b7deb4f9dc6a49a5f66a", - "reference": "e6af3e760864d43a30d8b7deb4f9dc6a49a5f66a", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/c04f8f30891cee8480c132778cd4cc486467e77a", + "reference": "c04f8f30891cee8480c132778cd4cc486467e77a", "shasum": "" }, "require": { @@ -1079,9 +1079,9 @@ "support": { "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.320.4" + "source": "https://github.com/aws/aws-sdk-php/tree/3.321.2" }, - "time": "2024-08-20T18:20:32+00:00" + "time": "2024-08-30T18:14:40+00:00" }, { "name": "bacon/bacon-qr-code", @@ -2603,16 +2603,16 @@ }, { "name": "filament/actions", - "version": "v3.2.105", + "version": "v3.2.110", "source": { "type": "git", "url": "https://github.com/filamentphp/actions.git", - "reference": "1ce746a4a75975f1844c175201f1f03443c48c95" + "reference": "5d6e4fe444f1ef04d373518248a445bbcc3ca272" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/actions/zipball/1ce746a4a75975f1844c175201f1f03443c48c95", - "reference": "1ce746a4a75975f1844c175201f1f03443c48c95", + "url": "https://api.github.com/repos/filamentphp/actions/zipball/5d6e4fe444f1ef04d373518248a445bbcc3ca272", + "reference": "5d6e4fe444f1ef04d373518248a445bbcc3ca272", "shasum": "" }, "require": { @@ -2652,20 +2652,20 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2024-08-14T16:52:38+00:00" + "time": "2024-08-26T07:22:35+00:00" }, { "name": "filament/filament", - "version": "v3.2.105", + "version": "v3.2.110", "source": { "type": "git", "url": "https://github.com/filamentphp/panels.git", - "reference": "2675472f2bdd4e765a1f3e533231bda7750a2881" + "reference": "130636e90e821154e0ce60dcbc7b358d2a1a716f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/panels/zipball/2675472f2bdd4e765a1f3e533231bda7750a2881", - "reference": "2675472f2bdd4e765a1f3e533231bda7750a2881", + "url": "https://api.github.com/repos/filamentphp/panels/zipball/130636e90e821154e0ce60dcbc7b358d2a1a716f", + "reference": "130636e90e821154e0ce60dcbc7b358d2a1a716f", "shasum": "" }, "require": { @@ -2717,20 +2717,20 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2024-08-20T08:32:50+00:00" + "time": "2024-08-30T01:52:09+00:00" }, { "name": "filament/forms", - "version": "v3.2.105", + "version": "v3.2.110", "source": { "type": "git", "url": "https://github.com/filamentphp/forms.git", - "reference": "23b1c5d35e3181d5c43d92be0005d0b320a2a96e" + "reference": "02fe2e211993f6291b719a093ed6f63e17125e9a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/forms/zipball/23b1c5d35e3181d5c43d92be0005d0b320a2a96e", - "reference": "23b1c5d35e3181d5c43d92be0005d0b320a2a96e", + "url": "https://api.github.com/repos/filamentphp/forms/zipball/02fe2e211993f6291b719a093ed6f63e17125e9a", + "reference": "02fe2e211993f6291b719a093ed6f63e17125e9a", "shasum": "" }, "require": { @@ -2773,11 +2773,11 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2024-08-20T08:32:33+00:00" + "time": "2024-08-30T18:04:06+00:00" }, { "name": "filament/infolists", - "version": "v3.2.105", + "version": "v3.2.110", "source": { "type": "git", "url": "https://github.com/filamentphp/infolists.git", @@ -2828,7 +2828,7 @@ }, { "name": "filament/notifications", - "version": "v3.2.105", + "version": "v3.2.110", "source": { "type": "git", "url": "https://github.com/filamentphp/notifications.git", @@ -2880,16 +2880,16 @@ }, { "name": "filament/support", - "version": "v3.2.105", + "version": "v3.2.110", "source": { "type": "git", "url": "https://github.com/filamentphp/support.git", - "reference": "544e521a310b1b4e7783d8ad1781ef7b1915232a" + "reference": "78e25428c754fcbb30c321d5dda439c760de9837" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/support/zipball/544e521a310b1b4e7783d8ad1781ef7b1915232a", - "reference": "544e521a310b1b4e7783d8ad1781ef7b1915232a", + "url": "https://api.github.com/repos/filamentphp/support/zipball/78e25428c754fcbb30c321d5dda439c760de9837", + "reference": "78e25428c754fcbb30c321d5dda439c760de9837", "shasum": "" }, "require": { @@ -2935,20 +2935,20 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2024-08-20T12:51:40+00:00" + "time": "2024-08-26T07:22:57+00:00" }, { "name": "filament/tables", - "version": "v3.2.105", + "version": "v3.2.110", "source": { "type": "git", "url": "https://github.com/filamentphp/tables.git", - "reference": "d801f70146d07fbf3cbbb9d9c09d8daf0ff530b5" + "reference": "129943d1b4e6c1edeef53e804eb56ef78a932a6c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/tables/zipball/d801f70146d07fbf3cbbb9d9c09d8daf0ff530b5", - "reference": "d801f70146d07fbf3cbbb9d9c09d8daf0ff530b5", + "url": "https://api.github.com/repos/filamentphp/tables/zipball/129943d1b4e6c1edeef53e804eb56ef78a932a6c", + "reference": "129943d1b4e6c1edeef53e804eb56ef78a932a6c", "shasum": "" }, "require": { @@ -2987,11 +2987,11 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2024-08-20T08:32:55+00:00" + "time": "2024-08-30T01:52:14+00:00" }, { "name": "filament/widgets", - "version": "v3.2.105", + "version": "v3.2.110", "source": { "type": "git", "url": "https://github.com/filamentphp/widgets.git", @@ -7361,16 +7361,16 @@ }, { "name": "opcodesio/log-viewer", - "version": "v3.10.2", + "version": "v3.11.1", "source": { "type": "git", "url": "https://github.com/opcodesio/log-viewer.git", - "reference": "8d44a049fce71753905ac4cd35e01c00544b17cb" + "reference": "608cde8a5fbac1e9959c060b780ef3ed26598aae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opcodesio/log-viewer/zipball/8d44a049fce71753905ac4cd35e01c00544b17cb", - "reference": "8d44a049fce71753905ac4cd35e01c00544b17cb", + "url": "https://api.github.com/repos/opcodesio/log-viewer/zipball/608cde8a5fbac1e9959c060b780ef3ed26598aae", + "reference": "608cde8a5fbac1e9959c060b780ef3ed26598aae", "shasum": "" }, "require": { @@ -7433,7 +7433,7 @@ ], "support": { "issues": "https://github.com/opcodesio/log-viewer/issues", - "source": "https://github.com/opcodesio/log-viewer/tree/v3.10.2" + "source": "https://github.com/opcodesio/log-viewer/tree/v3.11.1" }, "funding": [ { @@ -7445,7 +7445,7 @@ "type": "github" } ], - "time": "2024-08-03T17:20:45+00:00" + "time": "2024-08-23T07:25:44+00:00" }, { "name": "opcodesio/mail-parser", @@ -8201,16 +8201,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.29.1", + "version": "1.30.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "fcaefacf2d5c417e928405b71b400d4ce10daaf4" + "reference": "5ceb0e384997db59f38774bf79c2a6134252c08f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/fcaefacf2d5c417e928405b71b400d4ce10daaf4", - "reference": "fcaefacf2d5c417e928405b71b400d4ce10daaf4", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/5ceb0e384997db59f38774bf79c2a6134252c08f", + "reference": "5ceb0e384997db59f38774bf79c2a6134252c08f", "shasum": "" }, "require": { @@ -8242,9 +8242,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.29.1" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.30.0" }, - "time": "2024-05-31T08:52:43+00:00" + "time": "2024-08-29T09:54:52+00:00" }, { "name": "pragmarx/google2fa", @@ -8660,16 +8660,16 @@ }, { "name": "psr/log", - "version": "3.0.0", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001" + "reference": "79dff0b268932c640297f5208d6298f71855c03e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001", - "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001", + "url": "https://api.github.com/repos/php-fig/log/zipball/79dff0b268932c640297f5208d6298f71855c03e", + "reference": "79dff0b268932c640297f5208d6298f71855c03e", "shasum": "" }, "require": { @@ -8704,9 +8704,9 @@ "psr-3" ], "support": { - "source": "https://github.com/php-fig/log/tree/3.0.0" + "source": "https://github.com/php-fig/log/tree/3.0.1" }, - "time": "2021-07-14T16:46:02+00:00" + "time": "2024-08-21T13:31:24+00:00" }, { "name": "psr/simple-cache", @@ -10057,16 +10057,16 @@ }, { "name": "spatie/laravel-data", - "version": "4.8.1", + "version": "4.8.2", "source": { "type": "git", "url": "https://github.com/spatie/laravel-data.git", - "reference": "597961b02b2f7722246f21dad432bf24c2abb9d6" + "reference": "1732519693ac738cbc9cb21fdd00446c7a6a46e6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-data/zipball/597961b02b2f7722246f21dad432bf24c2abb9d6", - "reference": "597961b02b2f7722246f21dad432bf24c2abb9d6", + "url": "https://api.github.com/repos/spatie/laravel-data/zipball/1732519693ac738cbc9cb21fdd00446c7a6a46e6", + "reference": "1732519693ac738cbc9cb21fdd00446c7a6a46e6", "shasum": "" }, "require": { @@ -10129,7 +10129,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-data/issues", - "source": "https://github.com/spatie/laravel-data/tree/4.8.1" + "source": "https://github.com/spatie/laravel-data/tree/4.8.2" }, "funding": [ { @@ -10137,20 +10137,20 @@ "type": "github" } ], - "time": "2024-08-13T13:53:42+00:00" + "time": "2024-08-30T13:53:18+00:00" }, { "name": "spatie/laravel-medialibrary", - "version": "11.8.3", + "version": "11.9.1", "source": { "type": "git", "url": "https://github.com/spatie/laravel-medialibrary.git", - "reference": "453054178128b4e12e902651fe8f2afe37a5ba34" + "reference": "ff589ea5532a33d84faeb64bfdfd59057b4148b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-medialibrary/zipball/453054178128b4e12e902651fe8f2afe37a5ba34", - "reference": "453054178128b4e12e902651fe8f2afe37a5ba34", + "url": "https://api.github.com/repos/spatie/laravel-medialibrary/zipball/ff589ea5532a33d84faeb64bfdfd59057b4148b8", + "reference": "ff589ea5532a33d84faeb64bfdfd59057b4148b8", "shasum": "" }, "require": { @@ -10234,7 +10234,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-medialibrary/issues", - "source": "https://github.com/spatie/laravel-medialibrary/tree/11.8.3" + "source": "https://github.com/spatie/laravel-medialibrary/tree/11.9.1" }, "funding": [ { @@ -10246,7 +10246,7 @@ "type": "github" } ], - "time": "2024-08-20T09:30:53+00:00" + "time": "2024-09-02T06:32:15+00:00" }, { "name": "spatie/laravel-missing-page-redirector", @@ -10317,16 +10317,16 @@ }, { "name": "spatie/laravel-package-tools", - "version": "1.16.4", + "version": "1.16.5", "source": { "type": "git", "url": "https://github.com/spatie/laravel-package-tools.git", - "reference": "ddf678e78d7f8b17e5cdd99c0c3413a4a6592e53" + "reference": "c7413972cf22ffdff97b68499c22baa04eddb6a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/ddf678e78d7f8b17e5cdd99c0c3413a4a6592e53", - "reference": "ddf678e78d7f8b17e5cdd99c0c3413a4a6592e53", + "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/c7413972cf22ffdff97b68499c22baa04eddb6a2", + "reference": "c7413972cf22ffdff97b68499c22baa04eddb6a2", "shasum": "" }, "require": { @@ -10365,7 +10365,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-package-tools/issues", - "source": "https://github.com/spatie/laravel-package-tools/tree/1.16.4" + "source": "https://github.com/spatie/laravel-package-tools/tree/1.16.5" }, "funding": [ { @@ -10373,7 +10373,7 @@ "type": "github" } ], - "time": "2024-03-20T07:29:11+00:00" + "time": "2024-08-27T18:56:10+00:00" }, { "name": "spatie/laravel-permission", @@ -10958,16 +10958,16 @@ }, { "name": "spatie/php-structure-discoverer", - "version": "2.1.2", + "version": "2.2.0", "source": { "type": "git", "url": "https://github.com/spatie/php-structure-discoverer.git", - "reference": "0bfebf609b2047360cdca102d2c08fb78b393927" + "reference": "271542206169d95dd2ffe346ddf11f37672553a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/php-structure-discoverer/zipball/0bfebf609b2047360cdca102d2c08fb78b393927", - "reference": "0bfebf609b2047360cdca102d2c08fb78b393927", + "url": "https://api.github.com/repos/spatie/php-structure-discoverer/zipball/271542206169d95dd2ffe346ddf11f37672553a2", + "reference": "271542206169d95dd2ffe346ddf11f37672553a2", "shasum": "" }, "require": { @@ -11026,7 +11026,7 @@ ], "support": { "issues": "https://github.com/spatie/php-structure-discoverer/issues", - "source": "https://github.com/spatie/php-structure-discoverer/tree/2.1.2" + "source": "https://github.com/spatie/php-structure-discoverer/tree/2.2.0" }, "funding": [ { @@ -11034,7 +11034,7 @@ "type": "github" } ], - "time": "2024-08-13T15:00:59+00:00" + "time": "2024-08-29T10:43:45+00:00" }, { "name": "spatie/query-string", @@ -11474,16 +11474,16 @@ }, { "name": "symfony/console", - "version": "v7.1.3", + "version": "v7.1.4", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "cb1dcb30ebc7005c29864ee78adb47b5fb7c3cd9" + "reference": "1eed7af6961d763e7832e874d7f9b21c3ea9c111" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/cb1dcb30ebc7005c29864ee78adb47b5fb7c3cd9", - "reference": "cb1dcb30ebc7005c29864ee78adb47b5fb7c3cd9", + "url": "https://api.github.com/repos/symfony/console/zipball/1eed7af6961d763e7832e874d7f9b21c3ea9c111", + "reference": "1eed7af6961d763e7832e874d7f9b21c3ea9c111", "shasum": "" }, "require": { @@ -11547,7 +11547,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.1.3" + "source": "https://github.com/symfony/console/tree/v7.1.4" }, "funding": [ { @@ -11563,7 +11563,7 @@ "type": "tidelift" } ], - "time": "2024-07-26T12:41:01+00:00" + "time": "2024-08-15T22:48:53+00:00" }, { "name": "symfony/css-selector", @@ -11996,16 +11996,16 @@ }, { "name": "symfony/finder", - "version": "v7.1.3", + "version": "v7.1.4", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "717c6329886f32dc65e27461f80f2a465412fdca" + "reference": "d95bbf319f7d052082fb7af147e0f835a695e823" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/717c6329886f32dc65e27461f80f2a465412fdca", - "reference": "717c6329886f32dc65e27461f80f2a465412fdca", + "url": "https://api.github.com/repos/symfony/finder/zipball/d95bbf319f7d052082fb7af147e0f835a695e823", + "reference": "d95bbf319f7d052082fb7af147e0f835a695e823", "shasum": "" }, "require": { @@ -12040,7 +12040,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.1.3" + "source": "https://github.com/symfony/finder/tree/v7.1.4" }, "funding": [ { @@ -12056,7 +12056,7 @@ "type": "tidelift" } ], - "time": "2024-07-24T07:08:44+00:00" + "time": "2024-08-13T14:28:19+00:00" }, { "name": "symfony/html-sanitizer", @@ -12206,16 +12206,16 @@ }, { "name": "symfony/http-kernel", - "version": "v7.1.3", + "version": "v7.1.4", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "db9702f3a04cc471ec8c70e881825db26ac5f186" + "reference": "6efcbd1b3f444f631c386504fc83eeca25963747" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/db9702f3a04cc471ec8c70e881825db26ac5f186", - "reference": "db9702f3a04cc471ec8c70e881825db26ac5f186", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/6efcbd1b3f444f631c386504fc83eeca25963747", + "reference": "6efcbd1b3f444f631c386504fc83eeca25963747", "shasum": "" }, "require": { @@ -12300,7 +12300,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v7.1.3" + "source": "https://github.com/symfony/http-kernel/tree/v7.1.4" }, "funding": [ { @@ -12316,7 +12316,7 @@ "type": "tidelift" } ], - "time": "2024-07-26T14:58:15+00:00" + "time": "2024-08-30T17:02:28+00:00" }, { "name": "symfony/intl", @@ -12483,16 +12483,16 @@ }, { "name": "symfony/mime", - "version": "v7.1.2", + "version": "v7.1.4", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "26a00b85477e69a4bab63b66c5dce64f18b0cbfc" + "reference": "ccaa6c2503db867f472a587291e764d6a1e58758" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/26a00b85477e69a4bab63b66c5dce64f18b0cbfc", - "reference": "26a00b85477e69a4bab63b66c5dce64f18b0cbfc", + "url": "https://api.github.com/repos/symfony/mime/zipball/ccaa6c2503db867f472a587291e764d6a1e58758", + "reference": "ccaa6c2503db867f472a587291e764d6a1e58758", "shasum": "" }, "require": { @@ -12547,7 +12547,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.1.2" + "source": "https://github.com/symfony/mime/tree/v7.1.4" }, "funding": [ { @@ -12563,7 +12563,7 @@ "type": "tidelift" } ], - "time": "2024-06-28T10:03:55+00:00" + "time": "2024-08-13T14:28:19+00:00" }, { "name": "symfony/polyfill-ctype", @@ -13338,16 +13338,16 @@ }, { "name": "symfony/psr-http-message-bridge", - "version": "v7.1.3", + "version": "v7.1.4", "source": { "type": "git", "url": "https://github.com/symfony/psr-http-message-bridge.git", - "reference": "1365d10f5476f74a27cf9c2d1eee70c069019db0" + "reference": "405a7bcd872f1563966f64be19f1362d94ce71ab" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/1365d10f5476f74a27cf9c2d1eee70c069019db0", - "reference": "1365d10f5476f74a27cf9c2d1eee70c069019db0", + "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/405a7bcd872f1563966f64be19f1362d94ce71ab", + "reference": "405a7bcd872f1563966f64be19f1362d94ce71ab", "shasum": "" }, "require": { @@ -13401,7 +13401,7 @@ "psr-7" ], "support": { - "source": "https://github.com/symfony/psr-http-message-bridge/tree/v7.1.3" + "source": "https://github.com/symfony/psr-http-message-bridge/tree/v7.1.4" }, "funding": [ { @@ -13417,20 +13417,20 @@ "type": "tidelift" } ], - "time": "2024-07-17T06:10:24+00:00" + "time": "2024-08-15T22:48:53+00:00" }, { "name": "symfony/routing", - "version": "v7.1.3", + "version": "v7.1.4", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "8a908a3f22d5a1b5d297578c2ceb41b02fa916d0" + "reference": "1500aee0094a3ce1c92626ed8cf3c2037e86f5a7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/8a908a3f22d5a1b5d297578c2ceb41b02fa916d0", - "reference": "8a908a3f22d5a1b5d297578c2ceb41b02fa916d0", + "url": "https://api.github.com/repos/symfony/routing/zipball/1500aee0094a3ce1c92626ed8cf3c2037e86f5a7", + "reference": "1500aee0094a3ce1c92626ed8cf3c2037e86f5a7", "shasum": "" }, "require": { @@ -13482,7 +13482,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v7.1.3" + "source": "https://github.com/symfony/routing/tree/v7.1.4" }, "funding": [ { @@ -13498,7 +13498,7 @@ "type": "tidelift" } ], - "time": "2024-07-17T06:10:24+00:00" + "time": "2024-08-29T08:16:25+00:00" }, { "name": "symfony/service-contracts", @@ -13585,16 +13585,16 @@ }, { "name": "symfony/string", - "version": "v7.1.3", + "version": "v7.1.4", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "ea272a882be7f20cad58d5d78c215001617b7f07" + "reference": "6cd670a6d968eaeb1c77c2e76091c45c56bc367b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/ea272a882be7f20cad58d5d78c215001617b7f07", - "reference": "ea272a882be7f20cad58d5d78c215001617b7f07", + "url": "https://api.github.com/repos/symfony/string/zipball/6cd670a6d968eaeb1c77c2e76091c45c56bc367b", + "reference": "6cd670a6d968eaeb1c77c2e76091c45c56bc367b", "shasum": "" }, "require": { @@ -13652,7 +13652,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.1.3" + "source": "https://github.com/symfony/string/tree/v7.1.4" }, "funding": [ { @@ -13668,7 +13668,7 @@ "type": "tidelift" } ], - "time": "2024-07-22T10:25:37+00:00" + "time": "2024-08-12T09:59:40+00:00" }, { "name": "symfony/translation", @@ -13845,16 +13845,16 @@ }, { "name": "symfony/uid", - "version": "v7.1.1", + "version": "v7.1.4", "source": { "type": "git", "url": "https://github.com/symfony/uid.git", - "reference": "bb59febeecc81528ff672fad5dab7f06db8c8277" + "reference": "82177535395109075cdb45a70533aa3d7a521cdf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/uid/zipball/bb59febeecc81528ff672fad5dab7f06db8c8277", - "reference": "bb59febeecc81528ff672fad5dab7f06db8c8277", + "url": "https://api.github.com/repos/symfony/uid/zipball/82177535395109075cdb45a70533aa3d7a521cdf", + "reference": "82177535395109075cdb45a70533aa3d7a521cdf", "shasum": "" }, "require": { @@ -13899,7 +13899,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/uid/tree/v7.1.1" + "source": "https://github.com/symfony/uid/tree/v7.1.4" }, "funding": [ { @@ -13915,20 +13915,20 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:57:53+00:00" + "time": "2024-08-12T09:59:40+00:00" }, { "name": "symfony/var-dumper", - "version": "v7.1.3", + "version": "v7.1.4", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "86af4617cca75a6e28598f49ae0690f3b9d4591f" + "reference": "a5fa7481b199090964d6fd5dab6294d5a870c7aa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/86af4617cca75a6e28598f49ae0690f3b9d4591f", - "reference": "86af4617cca75a6e28598f49ae0690f3b9d4591f", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/a5fa7481b199090964d6fd5dab6294d5a870c7aa", + "reference": "a5fa7481b199090964d6fd5dab6294d5a870c7aa", "shasum": "" }, "require": { @@ -13982,7 +13982,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.1.3" + "source": "https://github.com/symfony/var-dumper/tree/v7.1.4" }, "funding": [ { @@ -13998,7 +13998,7 @@ "type": "tidelift" } ], - "time": "2024-07-26T12:41:01+00:00" + "time": "2024-08-30T16:12:47+00:00" }, { "name": "thecodingmachine/safe", @@ -15060,16 +15060,16 @@ }, { "name": "composer/pcre", - "version": "3.3.0", + "version": "3.3.1", "source": { "type": "git", "url": "https://github.com/composer/pcre.git", - "reference": "1637e067347a0c40bbb1e3cd786b20dcab556a81" + "reference": "63aaeac21d7e775ff9bc9d45021e1745c97521c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/1637e067347a0c40bbb1e3cd786b20dcab556a81", - "reference": "1637e067347a0c40bbb1e3cd786b20dcab556a81", + "url": "https://api.github.com/repos/composer/pcre/zipball/63aaeac21d7e775ff9bc9d45021e1745c97521c4", + "reference": "63aaeac21d7e775ff9bc9d45021e1745c97521c4", "shasum": "" }, "require": { @@ -15119,7 +15119,7 @@ ], "support": { "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.3.0" + "source": "https://github.com/composer/pcre/tree/3.3.1" }, "funding": [ { @@ -15135,7 +15135,7 @@ "type": "tidelift" } ], - "time": "2024-08-19T19:43:53+00:00" + "time": "2024-08-27T18:44:43+00:00" }, { "name": "driftingly/rector-laravel", @@ -15236,16 +15236,16 @@ }, { "name": "fidry/cpu-core-counter", - "version": "1.1.0", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/theofidry/cpu-core-counter.git", - "reference": "f92996c4d5c1a696a6a970e20f7c4216200fcc42" + "reference": "8520451a140d3f46ac33042715115e290cf5785f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/f92996c4d5c1a696a6a970e20f7c4216200fcc42", - "reference": "f92996c4d5c1a696a6a970e20f7c4216200fcc42", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/8520451a140d3f46ac33042715115e290cf5785f", + "reference": "8520451a140d3f46ac33042715115e290cf5785f", "shasum": "" }, "require": { @@ -15285,7 +15285,7 @@ ], "support": { "issues": "https://github.com/theofidry/cpu-core-counter/issues", - "source": "https://github.com/theofidry/cpu-core-counter/tree/1.1.0" + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.2.0" }, "funding": [ { @@ -15293,7 +15293,7 @@ "type": "github" } ], - "time": "2024-02-07T09:43:46+00:00" + "time": "2024-08-06T10:04:20+00:00" }, { "name": "filp/whoops", @@ -16135,16 +16135,16 @@ }, { "name": "phpmyadmin/sql-parser", - "version": "5.9.1", + "version": "5.10.0", "source": { "type": "git", "url": "https://github.com/phpmyadmin/sql-parser.git", - "reference": "169a9f11f1957ea36607c9b29eac1b48679f1ecc" + "reference": "91d980ab76c3f152481e367f62b921adc38af451" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpmyadmin/sql-parser/zipball/169a9f11f1957ea36607c9b29eac1b48679f1ecc", - "reference": "169a9f11f1957ea36607c9b29eac1b48679f1ecc", + "url": "https://api.github.com/repos/phpmyadmin/sql-parser/zipball/91d980ab76c3f152481e367f62b921adc38af451", + "reference": "91d980ab76c3f152481e367f62b921adc38af451", "shasum": "" }, "require": { @@ -16218,20 +16218,20 @@ "type": "other" } ], - "time": "2024-08-13T19:01:01+00:00" + "time": "2024-08-29T20:56:34+00:00" }, { "name": "phpstan/phpstan", - "version": "1.11.11", + "version": "1.12.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "707c2aed5d8d0075666e673a5e71440c1d01a5a3" + "reference": "384af967d35b2162f69526c7276acadce534d0e1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/707c2aed5d8d0075666e673a5e71440c1d01a5a3", - "reference": "707c2aed5d8d0075666e673a5e71440c1d01a5a3", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/384af967d35b2162f69526c7276acadce534d0e1", + "reference": "384af967d35b2162f69526c7276acadce534d0e1", "shasum": "" }, "require": { @@ -16276,36 +16276,36 @@ "type": "github" } ], - "time": "2024-08-19T14:37:29+00:00" + "time": "2024-08-27T09:18:05+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "10.1.15", + "version": "10.1.16", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "5da8b1728acd1e6ffdf2ff32ffbdfd04307f26ae" + "reference": "7e308268858ed6baedc8704a304727d20bc07c77" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/5da8b1728acd1e6ffdf2ff32ffbdfd04307f26ae", - "reference": "5da8b1728acd1e6ffdf2ff32ffbdfd04307f26ae", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7e308268858ed6baedc8704a304727d20bc07c77", + "reference": "7e308268858ed6baedc8704a304727d20bc07c77", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.18 || ^5.0", + "nikic/php-parser": "^4.19.1 || ^5.1.0", "php": ">=8.1", - "phpunit/php-file-iterator": "^4.0", - "phpunit/php-text-template": "^3.0", - "sebastian/code-unit-reverse-lookup": "^3.0", - "sebastian/complexity": "^3.0", - "sebastian/environment": "^6.0", - "sebastian/lines-of-code": "^2.0", - "sebastian/version": "^4.0", - "theseer/tokenizer": "^1.2.0" + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-text-template": "^3.0.1", + "sebastian/code-unit-reverse-lookup": "^3.0.0", + "sebastian/complexity": "^3.2.0", + "sebastian/environment": "^6.1.0", + "sebastian/lines-of-code": "^2.0.2", + "sebastian/version": "^4.0.1", + "theseer/tokenizer": "^1.2.3" }, "require-dev": { "phpunit/phpunit": "^10.1" @@ -16317,7 +16317,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "10.1-dev" + "dev-main": "10.1.x-dev" } }, "autoload": { @@ -16346,7 +16346,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.15" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.16" }, "funding": [ { @@ -16354,7 +16354,7 @@ "type": "github" } ], - "time": "2024-06-29T08:25:15+00:00" + "time": "2024-08-22T04:31:57+00:00" }, { "name": "phpunit/php-file-iterator", @@ -18115,16 +18115,16 @@ }, { "name": "symfony/yaml", - "version": "v7.1.1", + "version": "v7.1.4", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "fa34c77015aa6720469db7003567b9f772492bf2" + "reference": "92e080b851c1c655c786a2da77f188f2dccd0f4b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/fa34c77015aa6720469db7003567b9f772492bf2", - "reference": "fa34c77015aa6720469db7003567b9f772492bf2", + "url": "https://api.github.com/repos/symfony/yaml/zipball/92e080b851c1c655c786a2da77f188f2dccd0f4b", + "reference": "92e080b851c1c655c786a2da77f188f2dccd0f4b", "shasum": "" }, "require": { @@ -18166,7 +18166,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v7.1.1" + "source": "https://github.com/symfony/yaml/tree/v7.1.4" }, "funding": [ { @@ -18182,7 +18182,7 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:57:53+00:00" + "time": "2024-08-12T09:59:40+00:00" }, { "name": "theseer/tokenizer", From 7588b5d0d026998ca740a8393fd7017a01348d57 Mon Sep 17 00:00:00 2001 From: Wes Copeland Date: Wed, 4 Sep 2024 20:59:48 -0400 Subject: [PATCH 7/7] fix: restore red background on laravel-debugbar timings (#2671) --- composer.json | 2 +- composer.lock | 16 ++++++++-------- resources/css/utilities.css | 7 +++++++ 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/composer.json b/composer.json index 554baafa53..ab774ee5a8 100644 --- a/composer.json +++ b/composer.json @@ -70,7 +70,7 @@ "torann/geoip": "^3.0" }, "require-dev": { - "barryvdh/laravel-debugbar": "^3.7", + "barryvdh/laravel-debugbar": "^3.13.5", "barryvdh/laravel-ide-helper": "^3.1", "brianium/paratest": "^7.2", "driftingly/rector-laravel": "^0.26.0", diff --git a/composer.lock b/composer.lock index 03ebfefa58..dd47d26ad4 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d8f221057e549201b7173260c432261c", + "content-hash": "03530f8898321de285283b4e7646ba63", "packages": [ { "name": "amphp/amp", @@ -2002,16 +2002,16 @@ }, { "name": "doctrine/dbal", - "version": "3.9.0", + "version": "3.9.1", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "d8f68ea6cc00912e5313237130b8c8decf4d28c6" + "reference": "d7dc08f98cba352b2bab5d32c5e58f7e745c11a7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/d8f68ea6cc00912e5313237130b8c8decf4d28c6", - "reference": "d8f68ea6cc00912e5313237130b8c8decf4d28c6", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/d7dc08f98cba352b2bab5d32c5e58f7e745c11a7", + "reference": "d7dc08f98cba352b2bab5d32c5e58f7e745c11a7", "shasum": "" }, "require": { @@ -2027,7 +2027,7 @@ "doctrine/coding-standard": "12.0.0", "fig/log-test": "^1", "jetbrains/phpstorm-stubs": "2023.1", - "phpstan/phpstan": "1.11.7", + "phpstan/phpstan": "1.12.0", "phpstan/phpstan-strict-rules": "^1.6", "phpunit/phpunit": "9.6.20", "psalm/plugin-phpunit": "0.18.4", @@ -2095,7 +2095,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/3.9.0" + "source": "https://github.com/doctrine/dbal/tree/3.9.1" }, "funding": [ { @@ -2111,7 +2111,7 @@ "type": "tidelift" } ], - "time": "2024-08-15T07:34:42+00:00" + "time": "2024-09-01T13:49:23+00:00" }, { "name": "doctrine/deprecations", diff --git a/resources/css/utilities.css b/resources/css/utilities.css index f01b7ec074..6d18c410a8 100644 --- a/resources/css/utilities.css +++ b/resources/css/utilities.css @@ -11,6 +11,13 @@ @layer utilities { } +/** Properly show the red background on barryvdh/laravel-debugbar query timing measurements. **/ +div.phpdebugbar-widgets-sqlqueries div.phpdebugbar-widgets-bg-measure div.phpdebugbar-widgets-value { + height: 100% !important; + opacity: 0.2 !important; + background: red !important; +} + .bg-body { background-color: var(--bg-color); }
    {t('Last Post By')}{t('Message')}{t('Additional Posts')}Last Post ByMessageAdditional Posts