From ad6a9ba3862ec4336d40d5196a9871f62d5a18f6 Mon Sep 17 00:00:00 2001 From: Chew Date: Sun, 8 Feb 2026 01:37:41 -0600 Subject: [PATCH 1/7] feat: initial progress towards react inertia reorder site awards --- .../ReorderSiteAwardsController.php | 158 ++++++++++++ app/Community/RouteServiceProvider.php | 3 + .../+root/ReorderSiteAwardsMainRoot.tsx | 101 ++++++++ .../components/+root/index.ts | 1 + .../AwardOrderTable/AwardOrderTable.tsx | 228 ++++++++++++++++++ .../components/AwardOrderTable/index.ts | 1 + .../ManualMoveButtons/ManualMoveButtons.tsx | 59 +++++ .../components/ManualMoveButtons/index.ts | 1 + .../ResetOrderButton/ResetOrderButton.tsx | 42 ++++ .../components/ResetOrderButton/index.ts | 1 + resources/js/pages/reorder-site-awards.tsx | 24 ++ 11 files changed, 619 insertions(+) create mode 100644 app/Community/Controllers/ReorderSiteAwardsController.php create mode 100644 resources/js/features/reorder-site-awards/components/+root/ReorderSiteAwardsMainRoot.tsx create mode 100644 resources/js/features/reorder-site-awards/components/+root/index.ts create mode 100644 resources/js/features/reorder-site-awards/components/AwardOrderTable/AwardOrderTable.tsx create mode 100644 resources/js/features/reorder-site-awards/components/AwardOrderTable/index.ts create mode 100644 resources/js/features/reorder-site-awards/components/ManualMoveButtons/ManualMoveButtons.tsx create mode 100644 resources/js/features/reorder-site-awards/components/ManualMoveButtons/index.ts create mode 100644 resources/js/features/reorder-site-awards/components/ResetOrderButton/ResetOrderButton.tsx create mode 100644 resources/js/features/reorder-site-awards/components/ResetOrderButton/index.ts create mode 100644 resources/js/pages/reorder-site-awards.tsx diff --git a/app/Community/Controllers/ReorderSiteAwardsController.php b/app/Community/Controllers/ReorderSiteAwardsController.php new file mode 100644 index 0000000000..bfa557e57d --- /dev/null +++ b/app/Community/Controllers/ReorderSiteAwardsController.php @@ -0,0 +1,158 @@ +getUsersSiteAwards(request()->user()); + [$gameAwards, $eventAwards, $siteAwards, $eventData] = $this->SeparateAwards($awards); + + return Inertia::render('reorder-site-awards') + ->with('gameAwards', $gameAwards) + ->with('eventAwards', $eventAwards) + ->with('siteAwards', $siteAwards) + ->with('eventData', $eventData); + } + + public function getUsersSiteAwards(?User $user): array + { + $dbResult = []; + + if (! $user) { + return $dbResult; + } + + $bindings = [ + 'userId' => $user->id, + 'userId2' => $user->id, + 'userId3' => $user->id, + ]; + + $gameAwardValues = implode("','", AwardType::gameValues()); + + $query = " + -- game awards (mastery, beaten) + SELECT " . unixTimestampStatement('saw.awarded_at', 'AwardedAt') . ", saw.award_type, saw.user_id, saw.award_key, saw.award_tier, saw.order_column, gd.title AS Title, s.id AS ConsoleID, s.name AS ConsoleName, NULL AS Flags, gd.image_icon_asset_path AS ImageIcon + FROM user_awards AS saw + LEFT JOIN games AS gd ON ( gd.id = saw.award_key AND saw.award_type IN ('{$gameAwardValues}') ) + LEFT JOIN systems AS s ON s.id = gd.system_id + WHERE + saw.award_type IN('{$gameAwardValues}') + AND saw.user_id = :userId + GROUP BY saw.award_type, saw.award_key, saw.award_tier + HAVING + -- Remove duplicate game beaten awards. + (saw.award_type != '" . AwardType::GameBeaten->value . "' OR saw.award_tier = 1 OR NOT EXISTS ( + SELECT 1 FROM user_awards AS saw2 + WHERE saw2.award_type = saw.award_type AND saw2.award_key = saw.award_key AND saw2.award_tier = 1 AND saw2.user_id = saw.user_id + )) + -- Remove duplicate mastery awards. + AND (saw.award_type != '" . AwardType::Mastery->value . "' OR saw.award_tier = 1 OR NOT EXISTS ( + SELECT 1 FROM user_awards AS saw3 + WHERE saw3.award_type = saw.award_type AND saw3.award_key = saw.award_key AND saw3.award_tier = 1 AND saw3.user_id = saw.user_id + )) + UNION + -- event awards + SELECT " . unixTimestampStatement('saw.awarded_at', 'AwardedAt') . ", saw.award_type, saw.user_id, saw.award_key, saw.award_tier, saw.order_column, gd.title AS Title, " . System::Events . ", 'Events', NULL, e.image_asset_path AS ImageIcon + FROM user_awards AS saw + LEFT JOIN events e ON e.id = saw.award_key + LEFT JOIN games gd ON gd.id = e.legacy_game_id + WHERE + saw.award_type = '" . AwardType::Event->value . "' + AND saw.user_id = :userId3 + UNION + -- non-game awards (developer contribution, ...) + SELECT " . unixTimestampStatement('MAX(saw.awarded_at)', 'AwardedAt') . ", saw.award_type, saw.user_id, MAX( saw.award_key ), saw.award_tier, saw.order_column, NULL, NULL, NULL, NULL, NULL + FROM user_awards AS saw + WHERE + saw.award_type NOT IN('{$gameAwardValues}','" . AwardType::Event->value . "') + AND saw.user_id = :userId2 + GROUP BY saw.award_type + ORDER BY order_column, AwardedAt, award_type, award_tier ASC"; + + // TODO: Don't use legacy + $dbResult = legacyDbFetchAll($query, $bindings)->toArray(); + + foreach ($dbResult as &$award) { + unset($award['user_id']); + + $award['AwardType'] = AwardType::from($award['award_type'])->toLegacyInteger(); + $award['AwardData'] = (int) $award['award_key']; + $award['AwardDataExtra'] = (int) $award['award_tier']; + $award['DisplayOrder'] = (int) $award['order_column']; + + if ($award['ConsoleID']) { + $award['ConsoleID'] = (int) $award['ConsoleID']; + } + + unset($award['award_type'], $award['award_key'], $award['award_tier'], $award['order_column']); + } + + return $dbResult; + } + + public function SeparateAwards(array $userAwards): array + { + $awardEventGameIds = []; + $awardEventIds = []; + foreach ($userAwards as $award) { + $type = (int) $award['AwardType']; + if ($type === AwardType::Event->toLegacyInteger()) { + $awardEventIds[] = (int) $award['AwardData']; + } elseif (AwardType::isGame($type) && $award['ConsoleName'] === 'Events') { + $awardEventGameIds[] = (int) $award['AwardData']; + } + } + + if (!empty($awardEventGameIds)) { + $awardEventIds = array_merge($awardEventIds, + Event::whereIn('legacy_game_id', $awardEventIds)->select('id')->pluck('id')->toArray() + ); + } + + $eventData = new Collection(); + if (!empty($awardEventIds)) { + $eventData = Event::whereIn('id', $awardEventIds)->with('legacyGame')->get()->keyBy('id'); + } + + $gameAwards = []; // Mastery awards that aren't Events. + $eventAwards = []; // Event awards and Events mastery awards. + $siteAwards = []; // Dev event awards and non-game active awards. + + foreach ($userAwards as $award) { + $type = (int) $award['AwardType']; + $id = (int) $award['AwardData']; + + if (AwardType::isGame($type)) { + if ($award['ConsoleName'] === 'Events') { + $eventAwards[] = $award; + } elseif ($type !== AwardType::GameBeaten->toLegacyInteger()) { + $gameAwards[] = $award; + } + } elseif ($type === AwardType::Event->toLegacyInteger()) { + if ($eventData[$id]?->gives_site_award) { + $siteAwards[] = $award; + } else { + $eventAwards[] = $award; + } + } elseif (AwardType::isActive($type)) { + $siteAwards[] = $award; + } + } + + return [$gameAwards, $eventAwards, $siteAwards, $eventData]; + } + +} diff --git a/app/Community/RouteServiceProvider.php b/app/Community/RouteServiceProvider.php index d4f93544f9..f4ff357eaf 100755 --- a/app/Community/RouteServiceProvider.php +++ b/app/Community/RouteServiceProvider.php @@ -37,6 +37,7 @@ use App\Community\Controllers\LeaderboardCommentController; use App\Community\Controllers\MessageThreadController; use App\Community\Controllers\PatreonSupportersController; +use App\Community\Controllers\ReorderSiteAwardsController; use App\Community\Controllers\UnsubscribeController; use App\Community\Controllers\UserAchievementChecklistController; use App\Community\Controllers\UserCommentController; @@ -145,6 +146,8 @@ protected function mapWebRoutes(): void Route::get('messages/{user}/create', [MessageThreadController::class, 'create'])->name('message-thread.user.create'); Route::get('settings', [UserSettingsController::class, 'show'])->name('settings.show'); + + Route::get('reorder-site-awards', [ReorderSiteAwardsController::class, 'index'])->name('reorder-site-awards.index'); }); }); diff --git a/resources/js/features/reorder-site-awards/components/+root/ReorderSiteAwardsMainRoot.tsx b/resources/js/features/reorder-site-awards/components/+root/ReorderSiteAwardsMainRoot.tsx new file mode 100644 index 0000000000..d44a86d136 --- /dev/null +++ b/resources/js/features/reorder-site-awards/components/+root/ReorderSiteAwardsMainRoot.tsx @@ -0,0 +1,101 @@ +import type { FC } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { usePageProps } from '@/common/hooks/usePageProps'; +import { AwardOrderTable } from '@/features/reorder-site-awards/components/AwardOrderTable'; + +import { ResetOrderButton } from '../ResetOrderButton'; + +export interface AwardProps { + AwardData: number; + AwardDataExtra: number; + AwardType: number; + AwardedAt: number; + ConsoleID: number; + ConsoleName: string; + DisplayOrder: number; + ImageIcon: string; + Title: string; +} + +interface EventDataProps { + active_from: string; + active_through: string; + active_until: string; + created_at: string; + gives_site_award: boolean; + id: number; + image_asset_path: string; + legacy_game: { + achievement_set_version_hash: string; + achievements_published: number; + id: number; + image_box_art_asset_path: string; + image_icon_asset_path: string; + image_ingame_asset_path: string; + image_title_asset_path: string; + players_total: number; + points_total: number; + publisher: string; + sort_title: string; + system_id: number; + title: string; + updated_at: string; + }; + legacy_game_id: number; + updated_at: string; +} + +const ReorderSiteAwardsMainRoot: FC = () => { + const { gameAwards, siteAwards, eventAwards, eventData } = usePageProps<{ + gameAwards: AwardProps[]; + siteAwards: AwardProps[]; + eventAwards: AwardProps[]; + eventData: EventDataProps; + }>(); + console.log(gameAwards, siteAwards, eventAwards, eventData); + const { t } = useTranslation(); + + return ( +
+

{t('Reorder Site Awards')}

+ +
+

+ To rearrange your site awards, drag and drop the award rows or use the buttons within each + row to move them up or down. Award categories can be reordered using the dropdown menus + next to each category name. Remember to save your changes before leaving by clicking the + "Save All Changes" button. +

+
+ +
+
+ + +
+ +
+ + +
+ ); +}; + +export default ReorderSiteAwardsMainRoot; diff --git a/resources/js/features/reorder-site-awards/components/+root/index.ts b/resources/js/features/reorder-site-awards/components/+root/index.ts new file mode 100644 index 0000000000..abeeab141c --- /dev/null +++ b/resources/js/features/reorder-site-awards/components/+root/index.ts @@ -0,0 +1 @@ +export * from './ReorderSiteAwardsMainRoot'; diff --git a/resources/js/features/reorder-site-awards/components/AwardOrderTable/AwardOrderTable.tsx b/resources/js/features/reorder-site-awards/components/AwardOrderTable/AwardOrderTable.tsx new file mode 100644 index 0000000000..530e80a5df --- /dev/null +++ b/resources/js/features/reorder-site-awards/components/AwardOrderTable/AwardOrderTable.tsx @@ -0,0 +1,228 @@ +import React from 'react'; + +import type { AwardProps } from '@/features/reorder-site-awards/components/+root'; +import { ManualMoveButtons } from '@/features/reorder-site-awards/components/ManualMoveButtons'; + +interface AwardOrderTableProps { + title: string; + awards: AwardProps[]; + awardOwnerUsername: string; + awardCounterStart: number; + renderedSectionCount: number; + prefersSeeingSavedHiddenRows: boolean; + initialSectionOrder: number; + eventData: any; + reorderSiteAwards: any; // your drag + checkbox handlers + RenderAward: React.ComponentType; +} + +export const AwardOrderTable: React.FC = ({ + title, + awards, + awardOwnerUsername, + awardCounterStart, + renderedSectionCount, + prefersSeeingSavedHiddenRows, + initialSectionOrder, + eventData, + reorderSiteAwards, + RenderAward, +}) => { + const humanReadableAwardKind = title.split(' ')[0].toLowerCase(); + + let awardCounter = awardCounterStart; + + const renderAwardTitle = (award: AwardProps) => { + switch (award.AwardType) { + case 1: // Mastery (replace with enum if desired) + return {award.Title}; + + case 2: + return 'Achievements Earned by Others'; + + case 3: + return 'Achievement Points Earned by Others'; + + case 4: + return 'Patreon Supporter'; + + case 5: + return 'Certified Legend'; + + default: + return award.Title; + } + }; + + return ( + <> +
+

{title}

+ + +
+ + + + + + + + + + + + + {awards.map((award, index) => { + const awardDisplayOrder = award.DisplayOrder; + const isHiddenPreChecked = awardDisplayOrder === -1; + + const subduedOpacityClassName = isHiddenPreChecked ? 'opacity-40' : ''; + + const cursorGrabClass = isHiddenPreChecked ? '' : 'cursor-grab'; + + const savedHiddenClass = isHiddenPreChecked ? 'saved-hidden' : ''; + + const hiddenClass = !prefersSeeingSavedHiddenRows && isHiddenPreChecked ? 'hidden' : ''; + + const rowClassNames = ` + award-table-row + select-none + transition + ${cursorGrabClass} + ${savedHiddenClass} + ${hiddenClass} + `; + + const currentCounter = awardCounter++; + + return ( + + {/* Badge */} + + + {/* Title */} + + + {/* Hidden checkbox */} + + + {/* Manual Move */} + + + {/* Hidden inputs */} + + + + + ); + })} + +
BadgeSite AwardHidden + Manual Move +
+ + + {renderAwardTitle(award)} + + + reorderSiteAwards.handleRowHiddenCheckedChange(e, currentCounter) + } + /> + +
+ {awards.length > 50 && ( + <> + + + + + )} + + {awards.length > 15 && awards.length <= 50 && ( + <> + + + + )} + + {awards.length <= 15 && ( + + )} +
+
+ + ); +}; diff --git a/resources/js/features/reorder-site-awards/components/AwardOrderTable/index.ts b/resources/js/features/reorder-site-awards/components/AwardOrderTable/index.ts new file mode 100644 index 0000000000..d527b3e762 --- /dev/null +++ b/resources/js/features/reorder-site-awards/components/AwardOrderTable/index.ts @@ -0,0 +1 @@ +export * from './AwardOrderTable'; diff --git a/resources/js/features/reorder-site-awards/components/ManualMoveButtons/ManualMoveButtons.tsx b/resources/js/features/reorder-site-awards/components/ManualMoveButtons/ManualMoveButtons.tsx new file mode 100644 index 0000000000..57d148a57d --- /dev/null +++ b/resources/js/features/reorder-site-awards/components/ManualMoveButtons/ManualMoveButtons.tsx @@ -0,0 +1,59 @@ +import type { FC } from 'react'; + +interface ManualMoveButtonsProps { + awardCounter: number; + moveValue: number; + upLabel?: string; + downLabel?: string; + autoScroll?: boolean; + orientation?: 'vertical' | 'horizontal'; + isHiddenPreChecked?: boolean; +} + +export const ManualMoveButtons: FC = ({ + awardCounter, + isHiddenPreChecked, + moveValue, + autoScroll, + upLabel, + downLabel, + orientation, +}) => { + const downValue = moveValue; + const upValue = moveValue * -1; + + const containerClassNames = orientation === 'vertical' ? 'flex flex-col' : 'flex'; + + const rowsPlural = moveValue === 1 ? 'row' : 'rows'; + let upA11yLabel = `Move up ${moveValue} ${rowsPlural}`; + let downA11yLabel = `Move down ${moveValue} ${rowsPlural}`; + + if (moveValue > 10000) { + upA11yLabel = 'Move to top'; + downA11yLabel = 'Move to bottom'; + } + + return ( +
+ + + +
+ ); +}; diff --git a/resources/js/features/reorder-site-awards/components/ManualMoveButtons/index.ts b/resources/js/features/reorder-site-awards/components/ManualMoveButtons/index.ts new file mode 100644 index 0000000000..f25f4f68a7 --- /dev/null +++ b/resources/js/features/reorder-site-awards/components/ManualMoveButtons/index.ts @@ -0,0 +1 @@ +export * from './ManualMoveButtons'; diff --git a/resources/js/features/reorder-site-awards/components/ResetOrderButton/ResetOrderButton.tsx b/resources/js/features/reorder-site-awards/components/ResetOrderButton/ResetOrderButton.tsx new file mode 100644 index 0000000000..797a160b24 --- /dev/null +++ b/resources/js/features/reorder-site-awards/components/ResetOrderButton/ResetOrderButton.tsx @@ -0,0 +1,42 @@ +export const ResetOrderButton = () => { + return ( + + ); +}; + +export function handleResetOrder(): void { + if ( + !confirm( + 'This will resort all your awards by the date they were earned (oldest first). You can preview the changes before saving.', + ) + ) { + return; + } + + const rows = Array.from(document.querySelectorAll('.award-table-row')); + + // Sort rows by date of aquisition (ascending) + sortAwardsByAwardDate(rows); + + for (const row of rows) { + const awardKind = row.getAttribute('data-award-kind'); + document + .querySelector(`#${awardKind}-reorder-table`) + ?.querySelector('tbody') + ?.appendChild(row); + } +} + +const sortAwardsByAwardDate = (awards: Array): void => { + awards.sort((a, b) => { + const dateA = a.getAttribute('data-award-date') ?? '0'; + const dateB = b.getAttribute('data-award-date') ?? '0'; + + const numA = parseInt(dateA, 10); + const numB = parseInt(dateB, 10); + + return numA - numB; + }); +}; diff --git a/resources/js/features/reorder-site-awards/components/ResetOrderButton/index.ts b/resources/js/features/reorder-site-awards/components/ResetOrderButton/index.ts new file mode 100644 index 0000000000..16bd0fec22 --- /dev/null +++ b/resources/js/features/reorder-site-awards/components/ResetOrderButton/index.ts @@ -0,0 +1 @@ +export * from './ResetOrderButton'; diff --git a/resources/js/pages/reorder-site-awards.tsx b/resources/js/pages/reorder-site-awards.tsx new file mode 100644 index 0000000000..329a279036 --- /dev/null +++ b/resources/js/pages/reorder-site-awards.tsx @@ -0,0 +1,24 @@ +import { useTranslation } from 'react-i18next'; + +import { SEO } from '@/common/components/SEO'; +import { AppLayout } from '@/common/layouts/AppLayout'; +import type { AppPage } from '@/common/models'; +import ReorderSiteAwardsMainRoot from '@/features/reorder-site-awards/components/+root/ReorderSiteAwardsMainRoot'; + +const ReorderSiteAwards: AppPage = () => { + const { t } = useTranslation(); + + return ( + <> + + + + + + + ); +}; + +ReorderSiteAwards.layout = (page) => {page}; + +export default ReorderSiteAwards; From ccbd9c8ea33d732f42db4d2aeaf214d8b1ae4908 Mon Sep 17 00:00:00 2001 From: Chew Date: Tue, 10 Feb 2026 02:06:26 -0600 Subject: [PATCH 2/7] some more work, awards render now :) --- .../common/components/UserAward/UserAward.tsx | 29 +++++++++++++++++++ .../UserLegendAward/UserLegendAward.tsx | 25 ++++++++++++++++ .../UserPatreonAward/UserPatreonAward.tsx | 29 +++++++++++++++++++ .../js/common/components/UserAward/index.ts | 3 ++ .../UserAwardList/UserAwardList.tsx | 26 +++++++++++++++++ .../+root/ReorderSiteAwardsMainRoot.tsx | 24 +++++++-------- .../+sidebar/ReorderSiteAwardsSidebarRoot.tsx | 23 +++++++++++++++ .../components/+sidebar/index.ts | 1 + resources/js/pages/reorder-site-awards.tsx | 14 +++++++-- 9 files changed, 159 insertions(+), 15 deletions(-) create mode 100644 resources/js/common/components/UserAward/UserAward.tsx create mode 100644 resources/js/common/components/UserAward/UserLegendAward/UserLegendAward.tsx create mode 100644 resources/js/common/components/UserAward/UserPatreonAward/UserPatreonAward.tsx create mode 100644 resources/js/common/components/UserAward/index.ts create mode 100644 resources/js/common/components/UserAwardList/UserAwardList.tsx create mode 100644 resources/js/features/reorder-site-awards/components/+sidebar/ReorderSiteAwardsSidebarRoot.tsx create mode 100644 resources/js/features/reorder-site-awards/components/+sidebar/index.ts diff --git a/resources/js/common/components/UserAward/UserAward.tsx b/resources/js/common/components/UserAward/UserAward.tsx new file mode 100644 index 0000000000..bea7a2039e --- /dev/null +++ b/resources/js/common/components/UserAward/UserAward.tsx @@ -0,0 +1,29 @@ +import type { FC } from 'react'; + +export type AwardProps = { + imageUrl: string; + tooltip: string; + link?: string; + isGold?: boolean; + gameId?: number; + dateAwarded: string; +}; + +export const UserAward: FC<{ award: AwardProps; size?: number }> = ({ award, size = 64 }) => { + const img = ( + {award.tooltip} + ); + + return ( +
+ {award.link ? {img} : img} +
+ ); +}; diff --git a/resources/js/common/components/UserAward/UserLegendAward/UserLegendAward.tsx b/resources/js/common/components/UserAward/UserLegendAward/UserLegendAward.tsx new file mode 100644 index 0000000000..b1d92b1d54 --- /dev/null +++ b/resources/js/common/components/UserAward/UserLegendAward/UserLegendAward.tsx @@ -0,0 +1,25 @@ +import type { FC } from 'react'; + +import { asset } from '@/tall-stack/utils'; + +import type { AwardProps } from '../UserAward'; +import { UserAward } from '../UserAward'; + +export const UserLegendAward: FC<{ dateAwarded: string }> = ({ dateAwarded }) => { + /* + $tooltip = 'Specially Awarded to a Certified RetroAchievements Legend'; + $imagepath = asset('/assets/images/badge/legend.png'); + $imgclass = 'goldimage'; + $linkdest = ''; + */ + + const award: AwardProps = { + dateAwarded, + tooltip: 'Specially Awarded to a Certified RetroAchievements Legend!', + imageUrl: asset('/assets/images/badge/legend.png'), + isGold: true, + link: '', + }; + + return ; +}; diff --git a/resources/js/common/components/UserAward/UserPatreonAward/UserPatreonAward.tsx b/resources/js/common/components/UserAward/UserPatreonAward/UserPatreonAward.tsx new file mode 100644 index 0000000000..8658643b83 --- /dev/null +++ b/resources/js/common/components/UserAward/UserPatreonAward/UserPatreonAward.tsx @@ -0,0 +1,29 @@ +import type { FC } from 'react'; +import { route } from 'ziggy-js'; + +import { asset } from '@/tall-stack/utils'; + +import type { AwardProps } from '../UserAward'; +import { UserAward } from '../UserAward'; + +export const UserPatreonAward: FC<{ dateAwarded: string; size?: number }> = ({ + dateAwarded, + size, +}) => { + /* + $tooltip = 'Awarded for being a Patreon supporter! Thank-you so much for your support!'; + $imagepath = asset('/assets/images/badge/patreon.png'); + $imgclass = 'goldimage'; + $linkdest = route('patreon-supporter.index'); + */ + + const award: AwardProps = { + dateAwarded, + tooltip: 'Awarded for being a Patreon subscriber! Thank you fo much for your support!', + imageUrl: asset('/assets/images/badge/patreon.png'), + isGold: true, + link: route('patreon-supporter.index'), + }; + + return ; +}; diff --git a/resources/js/common/components/UserAward/index.ts b/resources/js/common/components/UserAward/index.ts new file mode 100644 index 0000000000..c4b7675008 --- /dev/null +++ b/resources/js/common/components/UserAward/index.ts @@ -0,0 +1,3 @@ +export * from './UserAward'; +export * from './UserLegendAward/UserLegendAward'; +export * from './UserPatreonAward/UserPatreonAward'; diff --git a/resources/js/common/components/UserAwardList/UserAwardList.tsx b/resources/js/common/components/UserAwardList/UserAwardList.tsx new file mode 100644 index 0000000000..7a4b2034c2 --- /dev/null +++ b/resources/js/common/components/UserAwardList/UserAwardList.tsx @@ -0,0 +1,26 @@ +import type { FC } from 'react'; +import React from 'react'; + +type UserAwardListProps = { + headingLabel: string; + headingCountSlot: React.ReactNode; + awards: React.ReactNode[]; +}; + +export const UserAwardList: FC = ({ + headingLabel, + headingCountSlot, + awards, +}) => { + return ( +
+

+ {headingLabel} + {headingCountSlot} +

+
+ {awards.map((award) => award)} +
+
+ ); +}; diff --git a/resources/js/features/reorder-site-awards/components/+root/ReorderSiteAwardsMainRoot.tsx b/resources/js/features/reorder-site-awards/components/+root/ReorderSiteAwardsMainRoot.tsx index d44a86d136..9bd9b6ec4c 100644 --- a/resources/js/features/reorder-site-awards/components/+root/ReorderSiteAwardsMainRoot.tsx +++ b/resources/js/features/reorder-site-awards/components/+root/ReorderSiteAwardsMainRoot.tsx @@ -46,7 +46,7 @@ interface EventDataProps { updated_at: string; } -const ReorderSiteAwardsMainRoot: FC = () => { +export const ReorderSiteAwardsMainRoot: FC = () => { const { gameAwards, siteAwards, eventAwards, eventData } = usePageProps<{ gameAwards: AwardProps[]; siteAwards: AwardProps[]; @@ -84,18 +84,16 @@ const ReorderSiteAwardsMainRoot: FC = () => { - + {/**/} ); }; - -export default ReorderSiteAwardsMainRoot; diff --git a/resources/js/features/reorder-site-awards/components/+sidebar/ReorderSiteAwardsSidebarRoot.tsx b/resources/js/features/reorder-site-awards/components/+sidebar/ReorderSiteAwardsSidebarRoot.tsx new file mode 100644 index 0000000000..b41b8713e8 --- /dev/null +++ b/resources/js/features/reorder-site-awards/components/+sidebar/ReorderSiteAwardsSidebarRoot.tsx @@ -0,0 +1,23 @@ +import { UserAward, UserPatreonAward } from '@/common/components/UserAward'; +import { UserAwardList } from '@/common/components/UserAwardList/UserAwardList'; + +export const ReorderSiteAwardsSidebarRoot = () => { + return ( + , + , + ]} + > + ); +}; diff --git a/resources/js/features/reorder-site-awards/components/+sidebar/index.ts b/resources/js/features/reorder-site-awards/components/+sidebar/index.ts new file mode 100644 index 0000000000..d75ed1bdea --- /dev/null +++ b/resources/js/features/reorder-site-awards/components/+sidebar/index.ts @@ -0,0 +1 @@ +export * from './ReorderSiteAwardsSidebarRoot'; diff --git a/resources/js/pages/reorder-site-awards.tsx b/resources/js/pages/reorder-site-awards.tsx index 329a279036..be00490048 100644 --- a/resources/js/pages/reorder-site-awards.tsx +++ b/resources/js/pages/reorder-site-awards.tsx @@ -3,18 +3,28 @@ import { useTranslation } from 'react-i18next'; import { SEO } from '@/common/components/SEO'; import { AppLayout } from '@/common/layouts/AppLayout'; import type { AppPage } from '@/common/models'; -import ReorderSiteAwardsMainRoot from '@/features/reorder-site-awards/components/+root/ReorderSiteAwardsMainRoot'; +import { ReorderSiteAwardsMainRoot } from '@/features/reorder-site-awards/components/+root'; +import { ReorderSiteAwardsSidebarRoot } from '@/features/reorder-site-awards/components/+sidebar'; const ReorderSiteAwards: AppPage = () => { const { t } = useTranslation(); return ( <> - + + + + + ); }; From b56eb366703ed2963b75a0d587dba7f88746090b Mon Sep 17 00:00:00 2001 From: Chew Date: Wed, 11 Feb 2026 21:58:59 -0600 Subject: [PATCH 3/7] award counter, start rendering real sidebar --- .../common/components/UserAward/UserAward.tsx | 4 +- .../UserAwardCounter/UserAwardCounter.tsx | 38 +++++++++ .../+root/ReorderSiteAwardsMainRoot.tsx | 4 +- .../+sidebar/ReorderSiteAwardsSidebarRoot.tsx | 84 +++++++++++++++---- 4 files changed, 110 insertions(+), 20 deletions(-) create mode 100644 resources/js/common/components/UserAwardCounter/UserAwardCounter.tsx diff --git a/resources/js/common/components/UserAward/UserAward.tsx b/resources/js/common/components/UserAward/UserAward.tsx index bea7a2039e..6f0768e679 100644 --- a/resources/js/common/components/UserAward/UserAward.tsx +++ b/resources/js/common/components/UserAward/UserAward.tsx @@ -1,6 +1,6 @@ import type { FC } from 'react'; -export type AwardProps = { +export type UserAwardProps = { imageUrl: string; tooltip: string; link?: string; @@ -9,7 +9,7 @@ export type AwardProps = { dateAwarded: string; }; -export const UserAward: FC<{ award: AwardProps; size?: number }> = ({ award, size = 64 }) => { +export const UserAward: FC<{ award: UserAwardProps; size?: number }> = ({ award, size = 64 }) => { const img = ( 0) { + $tooltip .= " ($numHidden hidden)"; + } + $counter = + "
+
$icon
$numItems
+
"; + + return $counter; +} + */ + +export const UserAwardCounter: FC = ({ icon, text, numItems, numHidden = 0 }) => { + let tooltip = `${numItems} ${text}`; + if (numHidden > 0) { + tooltip += ' ($numHidden hidden)'; + } + + return ( +
+
{icon}
+
{numItems}
+
+ ); +}; diff --git a/resources/js/features/reorder-site-awards/components/+root/ReorderSiteAwardsMainRoot.tsx b/resources/js/features/reorder-site-awards/components/+root/ReorderSiteAwardsMainRoot.tsx index 9bd9b6ec4c..8f7638f85b 100644 --- a/resources/js/features/reorder-site-awards/components/+root/ReorderSiteAwardsMainRoot.tsx +++ b/resources/js/features/reorder-site-awards/components/+root/ReorderSiteAwardsMainRoot.tsx @@ -18,7 +18,7 @@ export interface AwardProps { Title: string; } -interface EventDataProps { +export interface EventDataProps { active_from: string; active_through: string; active_until: string; @@ -57,7 +57,7 @@ export const ReorderSiteAwardsMainRoot: FC = () => { const { t } = useTranslation(); return ( -
+

{t('Reorder Site Awards')}

diff --git a/resources/js/features/reorder-site-awards/components/+sidebar/ReorderSiteAwardsSidebarRoot.tsx b/resources/js/features/reorder-site-awards/components/+sidebar/ReorderSiteAwardsSidebarRoot.tsx index b41b8713e8..71700e3062 100644 --- a/resources/js/features/reorder-site-awards/components/+sidebar/ReorderSiteAwardsSidebarRoot.tsx +++ b/resources/js/features/reorder-site-awards/components/+sidebar/ReorderSiteAwardsSidebarRoot.tsx @@ -1,23 +1,75 @@ +import { GameAvatar } from '@/common/components/GameAvatar'; +import type { UserAwardProps } from '@/common/components/UserAward'; import { UserAward, UserPatreonAward } from '@/common/components/UserAward'; +import { UserAwardCounter } from '@/common/components/UserAwardCounter/UserAwardCounter'; import { UserAwardList } from '@/common/components/UserAwardList/UserAwardList'; +import { usePageProps } from '@/common/hooks/usePageProps'; +import type { AwardProps, EventDataProps } from '@/features/reorder-site-awards/components/+root'; export const ReorderSiteAwardsSidebarRoot = () => { - return ( + const { gameAwards, siteAwards, eventAwards, eventData } = usePageProps<{ + gameAwards: AwardProps[]; + siteAwards: AwardProps[]; + eventAwards: AwardProps[]; + eventData: EventDataProps; + }>(); + + const gameAwardAwards = gameAwards + .sort((award) => award.DisplayOrder) + .map((award) => ( + + )); + + const gameAwardList = ( , - , - ]} - > + headingLabel={'Game Awards'} + headingCountSlot={ + + } + awards={gameAwardAwards} + /> + ); + + const siteAwardAwards = siteAwards + .sort((award) => award.DisplayOrder) + .map((award) => { + const newProps: UserAwardProps = { + dateAwarded: new Date(award.AwardedAt).toString(), + imageUrl: award.ImageIcon, + isGold: true, + tooltip: 'True', + }; + + return newProps; + }) + .map((award) => ); + + const siteAwardList = ( + + } + awards={siteAwardAwards} + /> + ); + + return ( +
+ {gameAwardList} + {siteAwardList} +
); }; From 89a4bd2e687dd65b8efa07cfd6b55fbe1545a63f Mon Sep 17 00:00:00 2001 From: Chew Date: Fri, 13 Feb 2026 16:39:33 -0600 Subject: [PATCH 4/7] actually enable sidebar lol --- resources/js/pages/reorder-site-awards.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/js/pages/reorder-site-awards.tsx b/resources/js/pages/reorder-site-awards.tsx index be00490048..af8d929717 100644 --- a/resources/js/pages/reorder-site-awards.tsx +++ b/resources/js/pages/reorder-site-awards.tsx @@ -29,6 +29,6 @@ const ReorderSiteAwards: AppPage = () => { ); }; -ReorderSiteAwards.layout = (page) => {page}; +ReorderSiteAwards.layout = (page) => {page}; export default ReorderSiteAwards; From f161e18b8b8d7821c7eeb236d9150b1470d11c1a Mon Sep 17 00:00:00 2001 From: Chew Date: Mon, 16 Feb 2026 16:57:27 -0600 Subject: [PATCH 5/7] sidebar is now basically finished! --- .../ReorderSiteAwardsController.php | 161 +++++++++++++++--- app/Community/Data/UserAwardData.php | 23 +++ .../common/components/UserAward/UserAward.tsx | 24 +-- .../UserLegendAward/UserLegendAward.tsx | 25 --- .../UserPatreonAward/UserPatreonAward.tsx | 29 ---- .../js/common/components/UserAward/index.ts | 2 - .../+root/ReorderSiteAwardsMainRoot.tsx | 37 +--- .../+sidebar/ReorderSiteAwardsSidebarRoot.tsx | 61 ++++--- resources/js/types/generated.d.ts | 11 ++ 9 files changed, 216 insertions(+), 157 deletions(-) create mode 100644 app/Community/Data/UserAwardData.php delete mode 100644 resources/js/common/components/UserAward/UserLegendAward/UserLegendAward.tsx delete mode 100644 resources/js/common/components/UserAward/UserPatreonAward/UserPatreonAward.tsx diff --git a/app/Community/Controllers/ReorderSiteAwardsController.php b/app/Community/Controllers/ReorderSiteAwardsController.php index bfa557e57d..036250c673 100644 --- a/app/Community/Controllers/ReorderSiteAwardsController.php +++ b/app/Community/Controllers/ReorderSiteAwardsController.php @@ -4,9 +4,12 @@ namespace App\Community\Controllers; +use App\Community\Data\UserAwardData; use App\Community\Enums\AwardType; use App\Http\Controller; use App\Models\Event; +use App\Models\EventAward; +use App\Models\PlayerBadge; use App\Models\System; use App\Models\User; use Illuminate\Support\Collection; @@ -17,23 +20,14 @@ class ReorderSiteAwardsController extends Controller public function index() { $awards = $this->getUsersSiteAwards(request()->user()); - [$gameAwards, $eventAwards, $siteAwards, $eventData] = $this->SeparateAwards($awards); + $cleanAwards = $this->SeparateAwards($awards); return Inertia::render('reorder-site-awards') - ->with('gameAwards', $gameAwards) - ->with('eventAwards', $eventAwards) - ->with('siteAwards', $siteAwards) - ->with('eventData', $eventData); + ->with('awards', $cleanAwards); } - public function getUsersSiteAwards(?User $user): array + public function getUsersSiteAwards(User $user): array { - $dbResult = []; - - if (! $user) { - return $dbResult; - } - $bindings = [ 'userId' => $user->id, 'userId2' => $user->id, @@ -88,7 +82,7 @@ public function getUsersSiteAwards(?User $user): array foreach ($dbResult as &$award) { unset($award['user_id']); - $award['AwardType'] = AwardType::from($award['award_type'])->toLegacyInteger(); + $award['AwardType'] = AwardType::from($award['award_type']); $award['AwardData'] = (int) $award['award_key']; $award['AwardDataExtra'] = (int) $award['award_tier']; $award['DisplayOrder'] = (int) $award['order_column']; @@ -103,27 +97,36 @@ public function getUsersSiteAwards(?User $user): array return $dbResult; } + /* + * array of ["AwardType" => enum AwardType, AwardData => int, AwardDataExtra => int, DisplayOrder => int, ConsoleID => int?] + */ + /** + * Parses awards into a usable state for the frontend. + * + * @param array $userAwards array of awards from the database. + * @return array + */ public function SeparateAwards(array $userAwards): array { $awardEventGameIds = []; $awardEventIds = []; foreach ($userAwards as $award) { - $type = (int) $award['AwardType']; - if ($type === AwardType::Event->toLegacyInteger()) { + $type = $award['AwardType']; + if ($type === AwardType::Event) { $awardEventIds[] = (int) $award['AwardData']; - } elseif (AwardType::isGame($type) && $award['ConsoleName'] === 'Events') { + } elseif ($award['ConsoleName'] === 'Events' && AwardType::isGame($type)) { $awardEventGameIds[] = (int) $award['AwardData']; } } - if (!empty($awardEventGameIds)) { + if (! empty($awardEventGameIds)) { $awardEventIds = array_merge($awardEventIds, Event::whereIn('legacy_game_id', $awardEventIds)->select('id')->pluck('id')->toArray() ); } - $eventData = new Collection(); - if (!empty($awardEventIds)) { + $eventData = new Collection; + if (! empty($awardEventIds)) { $eventData = Event::whereIn('id', $awardEventIds)->with('legacyGame')->get()->keyBy('id'); } @@ -131,28 +134,130 @@ public function SeparateAwards(array $userAwards): array $eventAwards = []; // Event awards and Events mastery awards. $siteAwards = []; // Dev event awards and non-game active awards. + /** @var UserAwardData[] $awards */ + $awards = []; + foreach ($userAwards as $award) { - $type = (int) $award['AwardType']; + $type = $award['AwardType']; $id = (int) $award['AwardData']; + $extra = (int) $award['AwardDataExtra']; + $awardDate = $award['AwardedAt']; + + $section = 'unknown'; if (AwardType::isGame($type)) { if ($award['ConsoleName'] === 'Events') { - $eventAwards[] = $award; - } elseif ($type !== AwardType::GameBeaten->toLegacyInteger()) { - $gameAwards[] = $award; + $section = 'event'; + } elseif ($type !== AwardType::GameBeaten) { + $section = 'game'; + $award["ImageIcon"] = asset($award['ImageIcon']); + $gameId = $id; + + $award["IsGold"] = $extra === 1; } - } elseif ($type === AwardType::Event->toLegacyInteger()) { + } elseif ($type === AwardType::Event) { if ($eventData[$id]?->gives_site_award) { - $siteAwards[] = $award; + $section = 'site'; } else { - $eventAwards[] = $award; + $section = 'event'; + } + + $event = $eventData->find($id); + if ($event) { + $tooltip = "Awarded for completing the $event->title event"; + $image = $event->image_asset_path; + + if ($extra !== 0) { + $eventAward = EventAward::where('event_id', $id) + ->where('tier_index', $extra) + ->first(); + + if ($eventAward) { + $image = $eventAward->image_asset_path; + + if ($eventAward->points_required < $event->legacyGame->points_total) { + $tooltip = "Awarded for earning at least $eventAward->points_required points in the $event->title event"; + } + } + } + + $award["Tooltip"] = $tooltip; + $award["ImageIcon"] = media_asset($image); + $award["IsGold"] = true; + $award["Link"] = route('event.show', $event->id); + /*
$tooltip

{$awardDate}

*/ } + } elseif (AwardType::isActive($type)) { - $siteAwards[] = $award; + $this->makeTooltip($award); + $section = 'site'; + } + + if ($section === 'unknown') { + continue; } + + $newAward = new UserAwardData( + imageUrl: $award['ImageIcon'] ?? '', + tooltip: $award['Tooltip'] ?? '', + link: $award['Link'] ?? '', + isGold: $award['IsGold'] ?? false, + gameId: $gameId ?? null, + dateAwarded: $award['AwardedAt'] . "", + awardType: $award['AwardType'], + awardSection: $section, + displayOrder: $award['DisplayOrder'], + ); + + $awards[] = $newAward; } - return [$gameAwards, $eventAwards, $siteAwards, $eventData]; + return $awards; } + public function makeTooltip(&$award): void + { + switch ($award['AwardType']) { + case AwardType::GameBeaten: + case AwardType::Mastery: + // no tooltip needed, frontend uses GameAvatar + $award['Tooltip'] = null; + + return; + case AwardType::AchievementPointsYield: + $data = $award["AwardData"]; + $points = PlayerBadge::getBadgeThreshold(AwardType::AchievementPointsYield, $data); + $award['Tooltip'] = "Awarded for producing many valuable achievements, providing over $points points to the community!"; + $award["ImageIcon"] = asset("/assets/images/badge/contribPoints-$data.png"); + $award["IsGold"] = true; + + return; + case AwardType::AchievementUnlocksYield: + $data = $award["AwardData"]; + $points = PlayerBadge::getBadgeThreshold(AwardType::AchievementUnlocksYield, $data); + $award['Tooltip'] = "Awarded for being a hard-working developer and producing achievements that have been earned over $points times!"; + $award["ImageIcon"] = asset("/assets/images/badge/contribYield-$data.png"); + $award["IsGold"] = true; + + return; + + case AwardType::CertifiedLegend: + $award['Tooltip'] = 'Specially Awarded to a Certified RetroAchievements Legend'; + $award["ImageIcon"] = asset('/assets/images/badge/legend.png'); + $award["IsGold"] = true; + + return; + case AwardType::PatreonSupporter: + $award['Tooltip'] = 'Awarded for being a Patreon supporter! Thank-you so much for your support!'; + $award["ImageIcon"] = asset('/assets/images/badge/patreon.png'); + $award["IsGold"] = true; + $award["Link"] = route('patreon-supporter.index'); + + return; + case AwardType::Event: + $award['Tooltip'] = 'Event Award'; + + return; + } + } } diff --git a/app/Community/Data/UserAwardData.php b/app/Community/Data/UserAwardData.php new file mode 100644 index 0000000000..343f80fcec --- /dev/null +++ b/app/Community/Data/UserAwardData.php @@ -0,0 +1,23 @@ + = ({ award, size = 64 }) => { +export const UserAward: FC<{ award: UserAwardData; size?: number }> = ({ award, size = 64 }) => { const img = ( = ({ award, /> ); + /* +
$tooltip

{$awardDate}

+ */ + return ( -
- {award.link ? {img} : img} +
+ {award.link ? {img} : img}
); }; diff --git a/resources/js/common/components/UserAward/UserLegendAward/UserLegendAward.tsx b/resources/js/common/components/UserAward/UserLegendAward/UserLegendAward.tsx deleted file mode 100644 index b1d92b1d54..0000000000 --- a/resources/js/common/components/UserAward/UserLegendAward/UserLegendAward.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import type { FC } from 'react'; - -import { asset } from '@/tall-stack/utils'; - -import type { AwardProps } from '../UserAward'; -import { UserAward } from '../UserAward'; - -export const UserLegendAward: FC<{ dateAwarded: string }> = ({ dateAwarded }) => { - /* - $tooltip = 'Specially Awarded to a Certified RetroAchievements Legend'; - $imagepath = asset('/assets/images/badge/legend.png'); - $imgclass = 'goldimage'; - $linkdest = ''; - */ - - const award: AwardProps = { - dateAwarded, - tooltip: 'Specially Awarded to a Certified RetroAchievements Legend!', - imageUrl: asset('/assets/images/badge/legend.png'), - isGold: true, - link: '', - }; - - return ; -}; diff --git a/resources/js/common/components/UserAward/UserPatreonAward/UserPatreonAward.tsx b/resources/js/common/components/UserAward/UserPatreonAward/UserPatreonAward.tsx deleted file mode 100644 index 8658643b83..0000000000 --- a/resources/js/common/components/UserAward/UserPatreonAward/UserPatreonAward.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import type { FC } from 'react'; -import { route } from 'ziggy-js'; - -import { asset } from '@/tall-stack/utils'; - -import type { AwardProps } from '../UserAward'; -import { UserAward } from '../UserAward'; - -export const UserPatreonAward: FC<{ dateAwarded: string; size?: number }> = ({ - dateAwarded, - size, -}) => { - /* - $tooltip = 'Awarded for being a Patreon supporter! Thank-you so much for your support!'; - $imagepath = asset('/assets/images/badge/patreon.png'); - $imgclass = 'goldimage'; - $linkdest = route('patreon-supporter.index'); - */ - - const award: AwardProps = { - dateAwarded, - tooltip: 'Awarded for being a Patreon subscriber! Thank you fo much for your support!', - imageUrl: asset('/assets/images/badge/patreon.png'), - isGold: true, - link: route('patreon-supporter.index'), - }; - - return ; -}; diff --git a/resources/js/common/components/UserAward/index.ts b/resources/js/common/components/UserAward/index.ts index c4b7675008..fcf78b00c3 100644 --- a/resources/js/common/components/UserAward/index.ts +++ b/resources/js/common/components/UserAward/index.ts @@ -1,3 +1 @@ export * from './UserAward'; -export * from './UserLegendAward/UserLegendAward'; -export * from './UserPatreonAward/UserPatreonAward'; diff --git a/resources/js/features/reorder-site-awards/components/+root/ReorderSiteAwardsMainRoot.tsx b/resources/js/features/reorder-site-awards/components/+root/ReorderSiteAwardsMainRoot.tsx index 8f7638f85b..1dbe02243d 100644 --- a/resources/js/features/reorder-site-awards/components/+root/ReorderSiteAwardsMainRoot.tsx +++ b/resources/js/features/reorder-site-awards/components/+root/ReorderSiteAwardsMainRoot.tsx @@ -5,6 +5,7 @@ import { usePageProps } from '@/common/hooks/usePageProps'; import { AwardOrderTable } from '@/features/reorder-site-awards/components/AwardOrderTable'; import { ResetOrderButton } from '../ResetOrderButton'; +import UserAwardData = App.Community.Data.UserAwardData; export interface AwardProps { AwardData: number; @@ -18,42 +19,10 @@ export interface AwardProps { Title: string; } -export interface EventDataProps { - active_from: string; - active_through: string; - active_until: string; - created_at: string; - gives_site_award: boolean; - id: number; - image_asset_path: string; - legacy_game: { - achievement_set_version_hash: string; - achievements_published: number; - id: number; - image_box_art_asset_path: string; - image_icon_asset_path: string; - image_ingame_asset_path: string; - image_title_asset_path: string; - players_total: number; - points_total: number; - publisher: string; - sort_title: string; - system_id: number; - title: string; - updated_at: string; - }; - legacy_game_id: number; - updated_at: string; -} - export const ReorderSiteAwardsMainRoot: FC = () => { - const { gameAwards, siteAwards, eventAwards, eventData } = usePageProps<{ - gameAwards: AwardProps[]; - siteAwards: AwardProps[]; - eventAwards: AwardProps[]; - eventData: EventDataProps; + const { awards } = usePageProps<{ + awards: UserAwardData[]; }>(); - console.log(gameAwards, siteAwards, eventAwards, eventData); const { t } = useTranslation(); return ( diff --git a/resources/js/features/reorder-site-awards/components/+sidebar/ReorderSiteAwardsSidebarRoot.tsx b/resources/js/features/reorder-site-awards/components/+sidebar/ReorderSiteAwardsSidebarRoot.tsx index 71700e3062..729cdf6889 100644 --- a/resources/js/features/reorder-site-awards/components/+sidebar/ReorderSiteAwardsSidebarRoot.tsx +++ b/resources/js/features/reorder-site-awards/components/+sidebar/ReorderSiteAwardsSidebarRoot.tsx @@ -1,30 +1,30 @@ import { GameAvatar } from '@/common/components/GameAvatar'; -import type { UserAwardProps } from '@/common/components/UserAward'; -import { UserAward, UserPatreonAward } from '@/common/components/UserAward'; +import { UserAward } from '@/common/components/UserAward'; import { UserAwardCounter } from '@/common/components/UserAwardCounter/UserAwardCounter'; import { UserAwardList } from '@/common/components/UserAwardList/UserAwardList'; import { usePageProps } from '@/common/hooks/usePageProps'; -import type { AwardProps, EventDataProps } from '@/features/reorder-site-awards/components/+root'; +import UserAwardData = App.Community.Data.UserAwardData; export const ReorderSiteAwardsSidebarRoot = () => { - const { gameAwards, siteAwards, eventAwards, eventData } = usePageProps<{ - gameAwards: AwardProps[]; - siteAwards: AwardProps[]; - eventAwards: AwardProps[]; - eventData: EventDataProps; + const { awards } = usePageProps<{ + awards: UserAwardData[]; }>(); - const gameAwardAwards = gameAwards - .sort((award) => award.DisplayOrder) + console.log('awards', awards); + + const gameAwardAwards = awards + .sort((award) => award.displayOrder) + .filter((award) => award.awardSection === 'game') .map((award) => ( )); @@ -33,7 +33,7 @@ export const ReorderSiteAwardsSidebarRoot = () => { headingLabel={'Game Awards'} headingCountSlot={ @@ -42,18 +42,9 @@ export const ReorderSiteAwardsSidebarRoot = () => { /> ); - const siteAwardAwards = siteAwards - .sort((award) => award.DisplayOrder) - .map((award) => { - const newProps: UserAwardProps = { - dateAwarded: new Date(award.AwardedAt).toString(), - imageUrl: award.ImageIcon, - isGold: true, - tooltip: 'True', - }; - - return newProps; - }) + const siteAwardAwards = awards + .sort((award) => award.displayOrder) + .filter((award) => award.awardSection === 'site') .map((award) => ); const siteAwardList = ( @@ -66,10 +57,26 @@ export const ReorderSiteAwardsSidebarRoot = () => { /> ); + const eventAwardAwards = awards + .sort((award) => award.displayOrder) + .filter((award) => award.awardSection === 'event') + .map((award) => ); + + const eventAwardList = ( + + } + awards={eventAwardAwards} + /> + ); + return (
{gameAwardList} {siteAwardList} + {eventAwardList}
); }; diff --git a/resources/js/types/generated.d.ts b/resources/js/types/generated.d.ts index 4c8a703266..0296059e07 100644 --- a/resources/js/types/generated.d.ts +++ b/resources/js/types/generated.d.ts @@ -165,6 +165,17 @@ declare namespace App.Community.Data { descriptionParams: Record | null; undoToken: string | null; }; + export type UserAwardData = { + imageUrl: string; + tooltip: string; + link: string | null; + isGold: boolean; + gameId: number | null; + dateAwarded: string; + awardType: App.Community.Enums.AwardType; + awardSection: string; + displayOrder: number; + }; export type UserGameListPageProps = { paginatedGameListEntries: App.Data.PaginatedData; filterableSystemOptions: Array; From a4cc5e54420ee86d6fc77d192f568c55e69cc1b0 Mon Sep 17 00:00:00 2001 From: Chew Date: Tue, 17 Feb 2026 22:34:26 -0600 Subject: [PATCH 6/7] bring over changes from event badge choosing --- .../ReorderSiteAwardsController.php | 52 ++++++++++++++----- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/app/Community/Controllers/ReorderSiteAwardsController.php b/app/Community/Controllers/ReorderSiteAwardsController.php index 036250c673..0f95d5f1d7 100644 --- a/app/Community/Controllers/ReorderSiteAwardsController.php +++ b/app/Community/Controllers/ReorderSiteAwardsController.php @@ -38,7 +38,7 @@ public function getUsersSiteAwards(User $user): array $query = " -- game awards (mastery, beaten) - SELECT " . unixTimestampStatement('saw.awarded_at', 'AwardedAt') . ", saw.award_type, saw.user_id, saw.award_key, saw.award_tier, saw.order_column, gd.title AS Title, s.id AS ConsoleID, s.name AS ConsoleName, NULL AS Flags, gd.image_icon_asset_path AS ImageIcon + SELECT " . unixTimestampStatement('saw.awarded_at', 'AwardedAt') . ", saw.award_type, saw.user_id, saw.award_key, saw.award_tier, saw.order_column, gd.title AS Title, s.id AS ConsoleID, s.name AS ConsoleName, NULL AS Flags, gd.image_icon_asset_path AS ImageIcon, NULL AS display_award_tier FROM user_awards AS saw LEFT JOIN games AS gd ON ( gd.id = saw.award_key AND saw.award_type IN ('{$gameAwardValues}') ) LEFT JOIN systems AS s ON s.id = gd.system_id @@ -59,7 +59,7 @@ public function getUsersSiteAwards(User $user): array )) UNION -- event awards - SELECT " . unixTimestampStatement('saw.awarded_at', 'AwardedAt') . ", saw.award_type, saw.user_id, saw.award_key, saw.award_tier, saw.order_column, gd.title AS Title, " . System::Events . ", 'Events', NULL, e.image_asset_path AS ImageIcon + SELECT " . unixTimestampStatement('saw.awarded_at', 'AwardedAt') . ", saw.award_type, saw.user_id, saw.award_key, saw.award_tier, saw.order_column, gd.title AS Title, " . System::Events . ", 'Events', NULL, e.image_asset_path AS ImageIcon, saw.display_award_tier FROM user_awards AS saw LEFT JOIN events e ON e.id = saw.award_key LEFT JOIN games gd ON gd.id = e.legacy_game_id @@ -68,7 +68,7 @@ public function getUsersSiteAwards(User $user): array AND saw.user_id = :userId3 UNION -- non-game awards (developer contribution, ...) - SELECT " . unixTimestampStatement('MAX(saw.awarded_at)', 'AwardedAt') . ", saw.award_type, saw.user_id, MAX( saw.award_key ), saw.award_tier, saw.order_column, NULL, NULL, NULL, NULL, NULL + SELECT " . unixTimestampStatement('MAX(saw.awarded_at)', 'AwardedAt') . ", saw.award_type, saw.user_id, MAX( saw.award_key ), saw.award_tier, saw.order_column, NULL, NULL, NULL, NULL, NULL, NULL FROM user_awards AS saw WHERE saw.award_type NOT IN('{$gameAwardValues}','" . AwardType::Event->value . "') @@ -126,8 +126,10 @@ public function SeparateAwards(array $userAwards): array } $eventData = new Collection; + $eventAwardData = new Collection; if (! empty($awardEventIds)) { $eventData = Event::whereIn('id', $awardEventIds)->with('legacyGame')->get()->keyBy('id'); + $eventAwardData = EventAward::whereIn('event_id', $awardEventIds)->get()->groupBy('event_id'); } $gameAwards = []; // Mastery awards that aren't Events. @@ -164,28 +166,50 @@ public function SeparateAwards(array $userAwards): array $event = $eventData->find($id); if ($event) { - $tooltip = "Awarded for completing the $event->title event"; + $tooltipTitle = $event->title; + $tooltipDescription = "Awarded for completing this event"; $image = $event->image_asset_path; - if ($extra !== 0) { - $eventAward = EventAward::where('event_id', $id) - ->where('tier_index', $extra) - ->first(); + // Use the display preference for the badge image, but always + // use the actual earned tier for the tooltip text. Otherwise, + // it's very ambiguous what tier the player is actually on if + // they have a saved tier preference. + $displayTier = (int) ($award['display_award_tier'] ?? $extra); + $actualTier = $extra; + + $tierIndicesToFetch = array_unique([$displayTier, $actualTier]); + $eventAwardsByTier = ($eventAwardData->get($id) ?? collect()) + ->whereIn('tier_index', $tierIndicesToFetch) + ->keyBy('tier_index'); + + $displayEventAward = $eventAwardsByTier->get($displayTier); + if ($displayEventAward) { + $image = $displayEventAward->image_asset_path; + } - if ($eventAward) { - $image = $eventAward->image_asset_path; + $actualEventAward = $eventAwardsByTier->get($actualTier); + if ($actualEventAward && $actualEventAward->points_required < $event->legacyGame->points_total) { + // Strip the event/game title prefix from the tier label to avoid duplication. + $tierLabel = $actualEventAward->label; + $gameTitle = $event->legacyGame->title ?? ''; + if ($tierLabel !== $gameTitle && str_starts_with($tierLabel, $gameTitle)) { + $tierLabel = ltrim(substr($tierLabel, strlen($gameTitle)), ' -:'); + } - if ($eventAward->points_required < $event->legacyGame->points_total) { - $tooltip = "Awarded for earning at least $eventAward->points_required points in the $event->title event"; - } + // Only append the tier label if the event title doesn't already contain it. + if (! str_ends_with($event->title, $tierLabel)) { + $tooltipTitle = "$event->title - $tierLabel"; } + + $tooltipDescription = "Awarded for earning at least $actualEventAward->points_required points"; } - $award["Tooltip"] = $tooltip; + $award["Tooltip"] = $tooltipTitle . "
" . $tooltipDescription; $award["ImageIcon"] = media_asset($image); $award["IsGold"] = true; $award["Link"] = route('event.show', $event->id); /*
$tooltip

{$awardDate}

*/ + // tooltip: "", } } elseif (AwardType::isActive($type)) { From 2bc34d220a93da1525ca506ad5dc23c688fa989f Mon Sep 17 00:00:00 2001 From: Chew Date: Sat, 21 Feb 2026 00:13:38 -0600 Subject: [PATCH 7/7] bunch of stuff --- .../ReorderSiteAwardsController.php | 9 +- app/Community/Data/UserAwardData.php | 1 + lang/en_US.json | 1 + package.json | 2 + pnpm-lock.yaml | 106 ++++-- .../+root/ReorderSiteAwardsMainRoot.tsx | 50 +-- .../+sidebar/ReorderSiteAwardsSidebarRoot.tsx | 18 +- .../AwardOrderTable/AwardOrderTable.tsx | 334 ++++++------------ .../components/AwardOrderTable/index.ts | 1 - .../AwardOrderTableOld/AwardOrderTableOld.tsx | 239 +++++++++++++ .../components/AwardOrderTableOld/index.ts | 1 + .../ManualMoveButtons/ManualMoveButtons.tsx | 10 +- resources/js/types/generated.d.ts | 1 + 13 files changed, 490 insertions(+), 283 deletions(-) delete mode 100644 resources/js/features/reorder-site-awards/components/AwardOrderTable/index.ts create mode 100644 resources/js/features/reorder-site-awards/components/AwardOrderTableOld/AwardOrderTableOld.tsx create mode 100644 resources/js/features/reorder-site-awards/components/AwardOrderTableOld/index.ts diff --git a/app/Community/Controllers/ReorderSiteAwardsController.php b/app/Community/Controllers/ReorderSiteAwardsController.php index 0f95d5f1d7..22e400f336 100644 --- a/app/Community/Controllers/ReorderSiteAwardsController.php +++ b/app/Community/Controllers/ReorderSiteAwardsController.php @@ -80,6 +80,8 @@ public function getUsersSiteAwards(User $user): array $dbResult = legacyDbFetchAll($query, $bindings)->toArray(); foreach ($dbResult as &$award) { + // dd($dbResult); + unset($award['user_id']); $award['AwardType'] = AwardType::from($award['award_type']); @@ -222,6 +224,7 @@ public function SeparateAwards(array $userAwards): array } $newAward = new UserAwardData( + title: $award["Title"], imageUrl: $award['ImageIcon'] ?? '', tooltip: $award['Tooltip'] ?? '', link: $award['Link'] ?? '', @@ -230,7 +233,7 @@ public function SeparateAwards(array $userAwards): array dateAwarded: $award['AwardedAt'] . "", awardType: $award['AwardType'], awardSection: $section, - displayOrder: $award['DisplayOrder'], + displayOrder: $award['DisplayOrder'] ); $awards[] = $newAward; @@ -251,6 +254,7 @@ public function makeTooltip(&$award): void case AwardType::AchievementPointsYield: $data = $award["AwardData"]; $points = PlayerBadge::getBadgeThreshold(AwardType::AchievementPointsYield, $data); + $award['Title'] = "Achievement Points Earned by Others"; $award['Tooltip'] = "Awarded for producing many valuable achievements, providing over $points points to the community!"; $award["ImageIcon"] = asset("/assets/images/badge/contribPoints-$data.png"); $award["IsGold"] = true; @@ -259,6 +263,7 @@ public function makeTooltip(&$award): void case AwardType::AchievementUnlocksYield: $data = $award["AwardData"]; $points = PlayerBadge::getBadgeThreshold(AwardType::AchievementUnlocksYield, $data); + $award["Title"] = "Achievements Earned by Others"; $award['Tooltip'] = "Awarded for being a hard-working developer and producing achievements that have been earned over $points times!"; $award["ImageIcon"] = asset("/assets/images/badge/contribYield-$data.png"); $award["IsGold"] = true; @@ -266,12 +271,14 @@ public function makeTooltip(&$award): void return; case AwardType::CertifiedLegend: + $award["Title"] = "Certified Legend"; $award['Tooltip'] = 'Specially Awarded to a Certified RetroAchievements Legend'; $award["ImageIcon"] = asset('/assets/images/badge/legend.png'); $award["IsGold"] = true; return; case AwardType::PatreonSupporter: + $award["Title"] = "Patreon Supporter"; $award['Tooltip'] = 'Awarded for being a Patreon supporter! Thank-you so much for your support!'; $award["ImageIcon"] = asset('/assets/images/badge/patreon.png'); $award["IsGold"] = true; diff --git a/app/Community/Data/UserAwardData.php b/app/Community/Data/UserAwardData.php index 343f80fcec..68fd6406ec 100644 --- a/app/Community/Data/UserAwardData.php +++ b/app/Community/Data/UserAwardData.php @@ -10,6 +10,7 @@ class UserAwardData extends Data { public function __construct( + public string $title, public string $imageUrl, public string $tooltip, public ?string $link, diff --git a/lang/en_US.json b/lang/en_US.json index 6a3e9cf5b4..7a2cc467b4 100644 --- a/lang/en_US.json +++ b/lang/en_US.json @@ -1309,6 +1309,7 @@ "Save": "Save", "Saving...": "Saving...", "Saved!": "Saved!", + "Save All Changes": "Save All Changes", "<1>If subsets aren't working or if every subset still requires a patch, make sure you're using the latest version of your emulator. If you're using RetroArch, make absolutely sure the emulator version is 1.22.1 or higher. Updating the RetroArch cores alone is not sufficient.": "<1>If subsets aren't working or if every subset still requires a patch, make sure you're using the latest version of your emulator. If you're using RetroArch, make absolutely sure the emulator version is 1.22.1 or higher. Updating the RetroArch cores alone is not sufficient.", "Opted In": "Opted In", "Opted Out": "Opted Out", diff --git a/package.json b/package.json index 8385c96b25..1435ccbb07 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,8 @@ "@bbob/plugin-helper": "^4.2.0", "@bbob/preset-react": "^4.2.0", "@bbob/react": "^4.2.0", + "@dnd-kit/helpers": "^0.3.2", + "@dnd-kit/react": "^0.3.2", "@floating-ui/core": "^1.6.8", "@floating-ui/dom": "^1.5.1", "@hookform/resolvers": "^5.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c84e5a9ecb..31fdf3365f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,12 @@ importers: '@bbob/react': specifier: ^4.2.0 version: 4.2.0(react@19.2.0) + '@dnd-kit/helpers': + specifier: ^0.3.2 + version: 0.3.2 + '@dnd-kit/react': + specifier: ^0.3.2 + version: 0.3.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@floating-ui/core': specifier: ^1.6.8 version: 1.6.8 @@ -604,6 +610,30 @@ packages: peerDependencies: postcss-selector-parser: ^6.0.13 + '@dnd-kit/abstract@0.3.2': + resolution: {integrity: sha512-uvPVK+SZYD6Viddn9M0K0JQdXknuVSxA/EbMlFRanve3P/XTc18oLa5zGftKSGjfQGmuzkZ34E26DSbly1zi3Q==} + + '@dnd-kit/collision@0.3.2': + resolution: {integrity: sha512-pNmNSLCI8S9fNQ7QJ3fBCDjiT0sqBhUFcKgmyYaGvGCAU+kq0AP8OWlh0JSisc9k5mFyxmRpmFQcnJpILz/RPA==} + + '@dnd-kit/dom@0.3.2': + resolution: {integrity: sha512-cIUAVgt2szQyz6JRy7I+0r+xeyOAGH21Y15hb5bIyHoDEaZBvIDH+OOlD9eoLjCbsxDLN9WloU2CBi3OE6LYDg==} + + '@dnd-kit/geometry@0.3.2': + resolution: {integrity: sha512-3UBPuIS7E3oGiHxOE8h810QA+0pnrnCtGxl4Os1z3yy5YkC/BEYGY+TxWPTQaY1/OMV7GCX7ZNMlama2QN3n3w==} + + '@dnd-kit/helpers@0.3.2': + resolution: {integrity: sha512-pj7pCE6BiysNetpPnzb3BJOrcKiqueUr1LFg6wYoi2fIFYpz66n2Ojd7HTwfwkpv0oyC3QlvA6Dk8cOmi6VavA==} + + '@dnd-kit/react@0.3.2': + resolution: {integrity: sha512-1Opg1xw6I75Z95c+rF2NJa0pdGb8rLAENtuopKtJ1J0PudWlz+P6yL137xy/6DV43uaRmNGtsdbMbR0yRYJ72g==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + + '@dnd-kit/state@0.3.2': + resolution: {integrity: sha512-dLUIkoYrIJhGXfF2wGLTfb46vUokEsO/OoE21TSfmahYrx7ysTmnwbePsznFaHlwgZhQEh6AlLvthLCeY21b1A==} + '@emnapi/core@1.4.4': resolution: {integrity: sha512-A9CnAbC6ARNMKcIcrQwq6HeHCjpcBZ5wSx4U01WXCqEKlrzB9F9315WDNHkrs2xbx7YjjSxbUYxuN6EQzpcY2g==} @@ -1130,6 +1160,9 @@ packages: '@polka/url@1.0.0-next.28': resolution: {integrity: sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==} + '@preact/signals-core@1.13.0': + resolution: {integrity: sha512-slT6XeTCAbdql61GVLlGU4x7XHI7kCZV5Um5uhE4zLX4ApgiiXc0UYFvVOKq06xcovzp7p+61l68oPi563ARKg==} + '@prisma/instrumentation@7.2.0': resolution: {integrity: sha512-Rh9Z4x5kEj1OdARd7U18AtVrnL6rmLSI0qYShaB4W7Wx5BKbgzndWF+QnuzMb7GLfVdlT5aYCXoPQVYuYtVu0g==} peerDependencies: @@ -1908,67 +1941,56 @@ packages: resolution: {integrity: sha512-54v4okehwl5TaSIkpp97rAHGp7t3ghinRd/vyC1iXqXMfjYUTm7TfYmCzXDoHUPTTf36L8pr0E7YsD3CfB3ZDg==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.50.1': resolution: {integrity: sha512-p/LaFyajPN/0PUHjv8TNyxLiA7RwmDoVY3flXHPSzqrGcIp/c2FjwPPP5++u87DGHtw+5kSH5bCJz0mvXngYxw==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.50.1': resolution: {integrity: sha512-2AbMhFFkTo6Ptna1zO7kAXXDLi7H9fGTbVaIq2AAYO7yzcAsuTNWPHhb2aTA6GPiP+JXh85Y8CiS54iZoj4opw==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.50.1': resolution: {integrity: sha512-Cgef+5aZwuvesQNw9eX7g19FfKX5/pQRIyhoXLCiBOrWopjo7ycfB292TX9MDcDijiuIJlx1IzJz3IoCPfqs9w==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loongarch64-gnu@4.50.1': resolution: {integrity: sha512-RPhTwWMzpYYrHrJAS7CmpdtHNKtt2Ueo+BlLBjfZEhYBhK00OsEqM08/7f+eohiF6poe0YRDDd8nAvwtE/Y62Q==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-ppc64-gnu@4.50.1': resolution: {integrity: sha512-eSGMVQw9iekut62O7eBdbiccRguuDgiPMsw++BVUg+1K7WjZXHOg/YOT9SWMzPZA+w98G+Fa1VqJgHZOHHnY0Q==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.50.1': resolution: {integrity: sha512-S208ojx8a4ciIPrLgazF6AgdcNJzQE4+S9rsmOmDJkusvctii+ZvEuIC4v/xFqzbuP8yDjn73oBlNDgF6YGSXQ==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.50.1': resolution: {integrity: sha512-3Ag8Ls1ggqkGUvSZWYcdgFwriy2lWo+0QlYgEFra/5JGtAd6C5Hw59oojx1DeqcA2Wds2ayRgvJ4qxVTzCHgzg==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.50.1': resolution: {integrity: sha512-t9YrKfaxCYe7l7ldFERE1BRg/4TATxIg+YieHQ966jwvo7ddHJxPj9cNFWLAzhkVsbBvNA4qTbPVNsZKBO4NSg==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.50.1': resolution: {integrity: sha512-MCgtFB2+SVNuQmmjHf+wfI4CMxy3Tk8XjA5Z//A0AKD7QXUYFMQcns91K6dEHBvZPCnhJSyDWLApk40Iq/H3tA==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.50.1': resolution: {integrity: sha512-nEvqG+0jeRmqaUMuwzlfMKwcIVffy/9KGbAGyoa26iu6eSngAYQ512bMXuqqPrlTyfqdlB9FVINs93j534UJrg==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-openharmony-arm64@4.50.1': resolution: {integrity: sha512-RDsLm+phmT3MJd9SNxA9MNuEAO/J2fhW8GXk62G/B4G7sLVumNFbRwDL6v5NrESb48k+QMqdGbHgEtfU0LCpbA==} @@ -2169,28 +2191,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.1.17': resolution: {integrity: sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.1.17': resolution: {integrity: sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.1.17': resolution: {integrity: sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.1.17': resolution: {integrity: sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==} @@ -2624,49 +2642,41 @@ packages: resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} cpu: [arm64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.11.1': resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} cpu: [arm64] os: [linux] - libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} cpu: [riscv64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} cpu: [riscv64] os: [linux] - libc: [musl] '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} cpu: [s390x] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.11.1': resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} cpu: [x64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.11.1': resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} cpu: [x64] os: [linux] - libc: [musl] '@unrs/resolver-binding-wasm32-wasi@1.11.1': resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} @@ -4308,28 +4318,24 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-musl@1.30.2: resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-x64-gnu@1.30.2: resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-musl@1.30.2: resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-win32-arm64-msvc@1.30.2: resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} @@ -6362,6 +6368,50 @@ snapshots: dependencies: postcss-selector-parser: 6.1.2 + '@dnd-kit/abstract@0.3.2': + dependencies: + '@dnd-kit/geometry': 0.3.2 + '@dnd-kit/state': 0.3.2 + tslib: 2.8.0 + + '@dnd-kit/collision@0.3.2': + dependencies: + '@dnd-kit/abstract': 0.3.2 + '@dnd-kit/geometry': 0.3.2 + tslib: 2.8.0 + + '@dnd-kit/dom@0.3.2': + dependencies: + '@dnd-kit/abstract': 0.3.2 + '@dnd-kit/collision': 0.3.2 + '@dnd-kit/geometry': 0.3.2 + '@dnd-kit/state': 0.3.2 + tslib: 2.8.0 + + '@dnd-kit/geometry@0.3.2': + dependencies: + '@dnd-kit/state': 0.3.2 + tslib: 2.8.0 + + '@dnd-kit/helpers@0.3.2': + dependencies: + '@dnd-kit/abstract': 0.3.2 + tslib: 2.8.0 + + '@dnd-kit/react@0.3.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@dnd-kit/abstract': 0.3.2 + '@dnd-kit/dom': 0.3.2 + '@dnd-kit/state': 0.3.2 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + tslib: 2.8.0 + + '@dnd-kit/state@0.3.2': + dependencies: + '@preact/signals-core': 1.13.0 + tslib: 2.8.0 + '@emnapi/core@1.4.4': dependencies: '@emnapi/wasi-threads': 1.0.3 @@ -6894,6 +6944,8 @@ snapshots: '@polka/url@1.0.0-next.28': {} + '@preact/signals-core@1.13.0': {} + '@prisma/instrumentation@7.2.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 diff --git a/resources/js/features/reorder-site-awards/components/+root/ReorderSiteAwardsMainRoot.tsx b/resources/js/features/reorder-site-awards/components/+root/ReorderSiteAwardsMainRoot.tsx index 1dbe02243d..65ff1f17b5 100644 --- a/resources/js/features/reorder-site-awards/components/+root/ReorderSiteAwardsMainRoot.tsx +++ b/resources/js/features/reorder-site-awards/components/+root/ReorderSiteAwardsMainRoot.tsx @@ -2,29 +2,31 @@ import type { FC } from 'react'; import { useTranslation } from 'react-i18next'; import { usePageProps } from '@/common/hooks/usePageProps'; -import { AwardOrderTable } from '@/features/reorder-site-awards/components/AwardOrderTable'; +import { AwardOrderTableOld } from '@/features/reorder-site-awards/components/AwardOrderTableOld'; import { ResetOrderButton } from '../ResetOrderButton'; import UserAwardData = App.Community.Data.UserAwardData; -export interface AwardProps { - AwardData: number; - AwardDataExtra: number; - AwardType: number; - AwardedAt: number; - ConsoleID: number; - ConsoleName: string; - DisplayOrder: number; - ImageIcon: string; - Title: string; -} - export const ReorderSiteAwardsMainRoot: FC = () => { const { awards } = usePageProps<{ awards: UserAwardData[]; }>(); const { t } = useTranslation(); + const saveAllChangesButton = () => { + const mappedTableRows = reorderSiteAwards.collectMappedTableRows(); + + try { + const withComputedDisplayOrderValues = + reorderSiteAwards.computeDisplayOrderValues(mappedTableRows); + + postAllAwardsDisplayOrder(withComputedDisplayOrderValues); + reorderSiteAwards.moveHiddenRowsToTop(); + } catch (error) { + showStatusFailure(error.toString()); + } + }; + return (

{t('Reorder Site Awards')}

@@ -50,19 +52,19 @@ export const ReorderSiteAwardsMainRoot: FC = () => {
- +
- {/**/} +
); }; diff --git a/resources/js/features/reorder-site-awards/components/+sidebar/ReorderSiteAwardsSidebarRoot.tsx b/resources/js/features/reorder-site-awards/components/+sidebar/ReorderSiteAwardsSidebarRoot.tsx index 729cdf6889..c9ca67306c 100644 --- a/resources/js/features/reorder-site-awards/components/+sidebar/ReorderSiteAwardsSidebarRoot.tsx +++ b/resources/js/features/reorder-site-awards/components/+sidebar/ReorderSiteAwardsSidebarRoot.tsx @@ -17,9 +17,9 @@ export const ReorderSiteAwardsSidebarRoot = () => { .filter((award) => award.awardSection === 'game') .map((award) => ( { /> )); + const masteries = awards.filter((award) => award.isGold).length; const gameAwardList = ( + <> + + + } awards={gameAwardAwards} /> diff --git a/resources/js/features/reorder-site-awards/components/AwardOrderTable/AwardOrderTable.tsx b/resources/js/features/reorder-site-awards/components/AwardOrderTable/AwardOrderTable.tsx index 530e80a5df..2fbb5529de 100644 --- a/resources/js/features/reorder-site-awards/components/AwardOrderTable/AwardOrderTable.tsx +++ b/resources/js/features/reorder-site-awards/components/AwardOrderTable/AwardOrderTable.tsx @@ -1,228 +1,120 @@ -import React from 'react'; - -import type { AwardProps } from '@/features/reorder-site-awards/components/+root'; -import { ManualMoveButtons } from '@/features/reorder-site-awards/components/ManualMoveButtons'; - -interface AwardOrderTableProps { - title: string; - awards: AwardProps[]; - awardOwnerUsername: string; - awardCounterStart: number; - renderedSectionCount: number; - prefersSeeingSavedHiddenRows: boolean; - initialSectionOrder: number; - eventData: any; - reorderSiteAwards: any; // your drag + checkbox handlers - RenderAward: React.ComponentType; -} - -export const AwardOrderTable: React.FC = ({ - title, - awards, - awardOwnerUsername, - awardCounterStart, - renderedSectionCount, - prefersSeeingSavedHiddenRows, - initialSectionOrder, - eventData, - reorderSiteAwards, - RenderAward, -}) => { - const humanReadableAwardKind = title.split(' ')[0].toLowerCase(); - - let awardCounter = awardCounterStart; - - const renderAwardTitle = (award: AwardProps) => { - switch (award.AwardType) { - case 1: // Mastery (replace with enum if desired) - return {award.Title}; - - case 2: - return 'Achievements Earned by Others'; - - case 3: - return 'Achievement Points Earned by Others'; - - case 4: - return 'Patreon Supporter'; - - case 5: - return 'Certified Legend'; - - default: - return award.Title; - } - }; +import type { FC } from 'react'; +import { useRef, useState } from 'react'; +import UserAwardData = App.Community.Data.UserAwardData; +import { move } from '@dnd-kit/helpers'; +import { DragDropProvider } from '@dnd-kit/react'; +import { useSortable } from '@dnd-kit/react/sortable'; +import { getCoreRowModel, useReactTable } from '@tanstack/react-table'; + +type Column = { id: string; name: string }; + +const columns: Column[] = [ + { id: 'imageUrl', name: 'Badge' }, + { id: 'title', name: 'Site Award' }, + { id: 'hidden', name: 'Hidden' }, + { id: 'manualMove', name: 'Manual Move' }, +]; +// https://tanstack.com/table/latest/docs/framework/react/examples/row-dnd?panel=sandbox +export const AwardOrderTable: FC<{ awards: UserAwardData[] }> = ({ awards }) => { + 'use no memo'; // useReactTable does not support React Compiler + + const [data, setData] = useState(awards); + + // eslint-disable-next-line react-hooks/incompatible-library -- https://github.com/TanStack/table/issues/5567 + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + }); + + const initialOrder = useRef({ + columns, + data, + }); return ( - <> -
-

{title}

- - + { + initialOrder.current = { + columns, + data, + }; + }} + onDragOver={(event) => { + const { source } = event.operation; + + setData((rows) => move(rows, event)); + }} + onDragEnd={(event) => { + if (event.canceled) { + // setData(initialOrder.current.rows); + } + }} + > +
+ + + + + ))} + + + + {data.map((row, index) => ( + + ))} + +
+ {columns.map((column, index) => ( + {column.name}
+
+ ); +}; - - - - - - - - - - - - {awards.map((award, index) => { - const awardDisplayOrder = award.DisplayOrder; - const isHiddenPreChecked = awardDisplayOrder === -1; - - const subduedOpacityClassName = isHiddenPreChecked ? 'opacity-40' : ''; - - const cursorGrabClass = isHiddenPreChecked ? '' : 'cursor-grab'; - - const savedHiddenClass = isHiddenPreChecked ? 'saved-hidden' : ''; - - const hiddenClass = !prefersSeeingSavedHiddenRows && isHiddenPreChecked ? 'hidden' : ''; - - const rowClassNames = ` - award-table-row - select-none - transition - ${cursorGrabClass} - ${savedHiddenClass} - ${hiddenClass} - `; - - const currentCounter = awardCounter++; - - return ( - - {/* Badge */} - - - {/* Title */} - - - {/* Hidden checkbox */} - - - {/* Manual Move */} - +function SortableRow({ row, columns, index, lastRow }: SortableRowProps) { + const { ref, handleRef, isDragging } = useSortable({ + id: row.gameId, + index, + type: 'row', + accept: 'row', + }); - {/* Hidden inputs */} - - - - - ); - })} - -
BadgeSite AwardHidden - Manual Move -
- - - {renderAwardTitle(award)} - - - reorderSiteAwards.handleRowHiddenCheckedChange(e, currentCounter) - } - /> - -
- {awards.length > 50 && ( - <> - - - - - )} - - {awards.length > 15 && awards.length <= 50 && ( - <> - - - - )} +interface SortableRowProps { + row: UserAwardData; + columns: Column[]; + index: number; + lastRow?: boolean; +} - {awards.length <= 15 && ( - - )} -
-
- + return ( + + True + {columns.map((column) => ( + True + ))} + ); -}; +} diff --git a/resources/js/features/reorder-site-awards/components/AwardOrderTable/index.ts b/resources/js/features/reorder-site-awards/components/AwardOrderTable/index.ts deleted file mode 100644 index d527b3e762..0000000000 --- a/resources/js/features/reorder-site-awards/components/AwardOrderTable/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './AwardOrderTable'; diff --git a/resources/js/features/reorder-site-awards/components/AwardOrderTableOld/AwardOrderTableOld.tsx b/resources/js/features/reorder-site-awards/components/AwardOrderTableOld/AwardOrderTableOld.tsx new file mode 100644 index 0000000000..f865532da0 --- /dev/null +++ b/resources/js/features/reorder-site-awards/components/AwardOrderTableOld/AwardOrderTableOld.tsx @@ -0,0 +1,239 @@ +import React, { useRef, useState } from 'react'; + +import { ManualMoveButtons } from '@/features/reorder-site-awards/components/ManualMoveButtons'; +import UserAwardData = App.Community.Data.UserAwardData; +import { DragDropProvider } from '@dnd-kit/react'; +import { useTranslation } from 'react-i18next'; + +import { UserAward } from '@/common/components/UserAward'; + +interface AwardOrderTableProps { + title: string; + awards: UserAwardData[]; + awardCounterStart: number; + renderedSectionCount: number; + prefersSeeingSavedHiddenRows: boolean; + initialSectionOrder: number; + reorderSiteAwards: any; // your drag + checkbox handlers +} + +const initialColumns: { id: string; name: string }[] = [ + { id: 'imageUrl', name: 'Badge' }, + { id: 'title', name: 'Site Award' }, + { id: 'hidden', name: 'Hidden' }, + { id: 'manualMove', name: 'Manual Move' }, +]; + +export const AwardOrderTableOld: React.FC = ({ + title, + awards, + awardCounterStart, + renderedSectionCount, + prefersSeeingSavedHiddenRows, + initialSectionOrder, + reorderSiteAwards, +}) => { + const humanReadableAwardKind = title.split(' ')[0].toLowerCase(); + + const { t } = useTranslation(); + + let awardCounter = awardCounterStart; + + const renderAwardTitle = (award: UserAwardData) => { + switch (award.awardType) { + case 'mastery': // Mastery (replace with enum if desired) + return {award.awardSection}; + + case 'achievement_unlocks_yield': + return 'Achievements Earned by Others'; + + case 'achievement_points_yield': + return 'Achievement Points Earned by Others'; + + case 'patreon_supporter': + return 'Patreon Supporter'; + + case 'certified_legend': + return 'Certified Legend'; + + default: + return award.tooltip; + } + }; + + const [rows, setRows] = useState(awards); + const [columns, setColumns] = useState(initialColumns); + const initialOrder = useRef({ + columns, + rows, + }); + + return ( + +
+

{title}

+ + +
+ + + + + + + + + + + + + + {awards.map((award, index) => { + const awardDisplayOrder = award.displayOrder; + const isHiddenPreChecked = awardDisplayOrder === -1; + + const subduedOpacityClassName = isHiddenPreChecked ? 'opacity-40' : ''; + + const cursorGrabClass = isHiddenPreChecked ? '' : 'cursor-grab'; + + const savedHiddenClass = isHiddenPreChecked ? 'saved-hidden' : ''; + + const hiddenClass = !prefersSeeingSavedHiddenRows && isHiddenPreChecked ? 'hidden' : ''; + + const rowClassNames = ` + award-table-row + select-none + transition + ${cursorGrabClass} + ${savedHiddenClass} + ${hiddenClass} + `; + + const currentCounter = awardCounter++; + + return ( + + {/* Badge */} + + + {/* Title */} + + + {/* Hidden checkbox */} + + + {/* Manual Move */} + + + {/* Hidden inputs */} + + {/**/} + {/**/} + + ); + })} + +
BadgeSite AwardHidden + Manual Move +
+ + + {renderAwardTitle(award)} + + false + // reorderSiteAwards.handleRowHiddenCheckedChange(e, currentCounter) + } + /> + +
+ {awards.length > 50 && ( + <> + + + + + )} + + {awards.length > 15 && awards.length <= 50 && ( + <> + + + + )} + + {awards.length <= 15 && ( + + )} +
+
+
+ ); +}; diff --git a/resources/js/features/reorder-site-awards/components/AwardOrderTableOld/index.ts b/resources/js/features/reorder-site-awards/components/AwardOrderTableOld/index.ts new file mode 100644 index 0000000000..e23f908f93 --- /dev/null +++ b/resources/js/features/reorder-site-awards/components/AwardOrderTableOld/index.ts @@ -0,0 +1 @@ +export * from './AwardOrderTableOld'; diff --git a/resources/js/features/reorder-site-awards/components/ManualMoveButtons/ManualMoveButtons.tsx b/resources/js/features/reorder-site-awards/components/ManualMoveButtons/ManualMoveButtons.tsx index 57d148a57d..91f6d29a31 100644 --- a/resources/js/features/reorder-site-awards/components/ManualMoveButtons/ManualMoveButtons.tsx +++ b/resources/js/features/reorder-site-awards/components/ManualMoveButtons/ManualMoveButtons.tsx @@ -1,6 +1,9 @@ import type { FC } from 'react'; +import UserAwardData = App.Community.Data.UserAwardData; +import { useTranslation } from 'react-i18next'; interface ManualMoveButtonsProps { + award: UserAwardData; awardCounter: number; moveValue: number; upLabel?: string; @@ -11,6 +14,7 @@ interface ManualMoveButtonsProps { } export const ManualMoveButtons: FC = ({ + award, awardCounter, isHiddenPreChecked, moveValue, @@ -19,6 +23,8 @@ export const ManualMoveButtons: FC = ({ downLabel, orientation, }) => { + const { t } = useTranslation(); + const downValue = moveValue; const upValue = moveValue * -1; @@ -39,7 +45,7 @@ export const ManualMoveButtons: FC = ({ title={upA11yLabel} aria-label={upA11yLabel} className="btn py-0.5 text-2xs" - onClick="reorderSiteAwards.moveRow($awardCounter, $upValue, $autoScroll)" + onClick={() => (award.displayOrder += upValue)} disabled={isHiddenPreChecked} > ↑{upLabel} @@ -49,7 +55,7 @@ export const ManualMoveButtons: FC = ({ title={downA11yLabel} aria-label={downA11yLabel} className="btn py-0.5 text-2xs" - onClick="reorderSiteAwards.moveRow($awardCounter, $downValue, $autoScroll)" + onClick={() => (award.displayOrder += downValue)} disabled={isHiddenPreChecked} > ↓{downLabel} diff --git a/resources/js/types/generated.d.ts b/resources/js/types/generated.d.ts index 100fcfc992..4115d3c6b2 100644 --- a/resources/js/types/generated.d.ts +++ b/resources/js/types/generated.d.ts @@ -167,6 +167,7 @@ declare namespace App.Community.Data { undoToken: string | null; }; export type UserAwardData = { + title: string; imageUrl: string; tooltip: string; link: string | null;