diff --git a/app/Community/Components/UserProfileMeta.php b/app/Community/Components/UserProfileMeta.php index 347c1e1ef5..eef6828f94 100644 --- a/app/Community/Components/UserProfileMeta.php +++ b/app/Community/Components/UserProfileMeta.php @@ -12,6 +12,7 @@ use App\Platform\Enums\PlayerStatType; use Illuminate\Contracts\View\View; use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; use Illuminate\View\Component; @@ -322,13 +323,13 @@ private function buildRankMetadata( private function buildSocialStats(User $user): array { $userSetRequestInformation = getUserRequestsInformation($user); - $numForumPosts = $user->forumPosts()->count(); + $numForumPosts = $user->forumPosts()->authorized()->viewable(Auth::user())->count(); // Forum posts $forumPostsStat = [ 'label' => 'Forum posts', 'value' => localized_number($numForumPosts), - 'href' => $numForumPosts ? route('user.posts', ['user' => $user]) : null, + 'href' => $numForumPosts ? route('user.posts.index', ['user' => $user]) : null, 'isMuted' => $numForumPosts === 0, ]; diff --git a/app/Community/Controllers/ForumTopicCommentController.php b/app/Community/Controllers/ForumTopicCommentController.php index 475ad8be8b..5df35e5ac1 100644 --- a/app/Community/Controllers/ForumTopicCommentController.php +++ b/app/Community/Controllers/ForumTopicCommentController.php @@ -7,22 +7,13 @@ use App\Community\Actions\AddCommentAction; use App\Community\Actions\GetUrlToCommentDestinationAction; use App\Community\Requests\ForumTopicCommentRequest; -use App\Community\Services\ForumRecentPostsPageService; use App\Models\ForumTopic; use App\Models\ForumTopicComment; -use App\Support\Shortcode\Shortcode; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; -use Inertia\Inertia; -use Inertia\Response as InertiaResponse; class ForumTopicCommentController extends CommentController { - public function __construct( - protected ForumRecentPostsPageService $recentPostsPageService - ) { - } - /** * There is no create form for creating a new comment. * comments have to be created for something -> use sub resource create route, e.g. @@ -92,46 +83,4 @@ protected function destroy(ForumTopicComment $comment): RedirectResponse return redirect($return) ->with('success', $this->resourceActionSuccessMessage('comment', 'delete')); } - - public function showRecentPosts(): InertiaResponse - { - $this->authorize('viewAny', ForumTopicComment::class); - - // TODO after POC: thin this out - $pageProps = $this->recentPostsPageService->buildViewData( - request()->user(), - (int) request()->input('offset', 0) - ); - - // TODO after POC: migrate the query to Eloquent ORM and have it do these mappings automatically - $mappedRecentForumPosts = []; - foreach ($pageProps['recentForumPosts'] as &$recentForumPost) { - $mappedRecentForumPost = []; - - $mappedRecentForumPost['forumTopicId'] = (int) $recentForumPost['ForumTopicID']; - $mappedRecentForumPost['forumTopicTitle'] = $recentForumPost['ForumTopicTitle']; - $mappedRecentForumPost['commentId'] = (int) $recentForumPost['CommentID']; - $mappedRecentForumPost['postedAt'] = $recentForumPost['PostedAt']; - $mappedRecentForumPost['authorDisplayName'] = $recentForumPost['Author']; - $mappedRecentForumPost['shortMessage'] = Shortcode::stripAndClamp($recentForumPost['ShortMsg'], 999); - $mappedRecentForumPost['commentIdDay'] = isset($recentForumPost['CommentID_1d']) ? (int) $recentForumPost['CommentID_1d'] : null; - $mappedRecentForumPost['commentCountDay'] = isset($recentForumPost['Count_1d']) ? (int) $recentForumPost['Count_1d'] : null; - $mappedRecentForumPost['commentIdWeek'] = isset($recentForumPost['CommentID_7d']) ? (int) $recentForumPost['CommentID_7d'] : null; - $mappedRecentForumPost['commentCountWeek'] = isset($recentForumPost['Count_7d']) ? (int) $recentForumPost['Count_7d'] : null; - - $mappedRecentForumPosts[] = $mappedRecentForumPost; - } - unset($pageProps['recentForumPosts']); - $pageProps['recentForumPosts'] = $mappedRecentForumPosts; - - // TODO after POC: remove this code - if (isset($pageProps['previousPageUrl'])) { - $pageProps['previousPageUrl'] = str_replace('recent-posts', 'recent-posts2', $pageProps['previousPageUrl']); - } - if (isset($pageProps['nextPageUrl'])) { - $pageProps['nextPageUrl'] = str_replace('recent-posts', 'recent-posts2', $pageProps['nextPageUrl']); - } - - return Inertia::render('forums/recent-posts', $pageProps); - } } diff --git a/app/Community/Controllers/UserForumTopicCommentController.php b/app/Community/Controllers/UserForumTopicCommentController.php new file mode 100644 index 0000000000..3b8137399f --- /dev/null +++ b/app/Community/Controllers/UserForumTopicCommentController.php @@ -0,0 +1,103 @@ +authorize('view', $user); + + $offset = $request->input('page', 1) - 1; + $count = 25; + + /** @var User $me */ + $me = auth()->user(); + $permissions = Permissions::Unregistered; + if ($me) { + $permissions = (int) $me->getAttribute('Permissions'); + } + + $posts = $this->getUserPosts( + $user, + page: (int) $request->input('page', 1), + permissions: $permissions, + ); + + $transformedPosts = array_map( + fn ($post) => ForumTopicData::fromUserPost($post)->include('latestComment'), + $posts + ); + + $paginator = new LengthAwarePaginator( + items: $transformedPosts, + total: $user->forumPosts()->authorized()->viewable($me)->count(), + perPage: $count, + currentPage: $offset + 1, + options: [ + 'path' => $request->url(), + 'query' => $request->query(), + ] + ); + + $paginatedPosts = PaginatedData::fromLengthAwarePaginator($paginator); + + $props = new UserRecentPostsPagePropsData( + UserData::fromUser($user), + $paginatedPosts + ); + + return Inertia::render('user/[user]/posts', $props); + } + + private function getUserPosts(User $user, int $page = 1, int $permissions = Permissions::Unregistered): array + { + $count = 25; + $offset = ($page - 1) * $count; + + $query = " + SELECT + ft.ID AS ForumTopicID, + ft.Title AS ForumTopicTitle, + f.ID AS ForumID, + f.Title AS ForumTitle, + lftc.ID AS CommentID, + lftc.DateCreated AS PostedAt, + lftc.author_id, + ua.User AS Author, + ua.display_name AS AuthorDisplayName, + LEFT(lftc.Payload, 260) AS ShortMsg, + LENGTH(lftc.Payload) > 260 AS IsTruncated + FROM ForumTopicComment AS lftc + INNER JOIN ForumTopic AS ft ON ft.ID = lftc.ForumTopicID + INNER JOIN Forum AS f ON f.ID = ft.ForumID + LEFT JOIN UserAccounts AS ua ON ua.ID = lftc.author_id + WHERE lftc.author_id = :author_id + AND lftc.Authorised = 1 + AND ft.RequiredPermissions <= :permissions + AND ft.deleted_at IS NULL + ORDER BY lftc.DateCreated DESC + LIMIT :offset, :count"; + + return legacyDbFetchAll($query, [ + 'author_id' => $user->id, + 'offset' => $offset, + 'count' => $count, + 'permissions' => $permissions, + ])->toArray(); + } +} diff --git a/app/Community/Data/UserRecentPostsPagePropsData.php b/app/Community/Data/UserRecentPostsPagePropsData.php new file mode 100644 index 0000000000..dcdf38d7b3 --- /dev/null +++ b/app/Community/Data/UserRecentPostsPagePropsData.php @@ -0,0 +1,20 @@ +')] +class UserRecentPostsPagePropsData extends Data +{ + public function __construct( + public UserData $targetUser, + public PaginatedData $paginatedTopics, + ) { + } +} diff --git a/app/Community/RouteServiceProvider.php b/app/Community/RouteServiceProvider.php index 1558a27d89..eda84f7e04 100755 --- a/app/Community/RouteServiceProvider.php +++ b/app/Community/RouteServiceProvider.php @@ -9,6 +9,7 @@ use App\Community\Controllers\MessageController; use App\Community\Controllers\MessageThreadController; use App\Community\Controllers\UserCommentController; +use App\Community\Controllers\UserForumTopicCommentController; use App\Community\Controllers\UserSettingsController; use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider; use Illuminate\Support\Facades\Route; @@ -41,6 +42,8 @@ protected function mapWebRoutes(): void Route::middleware(['inertia'])->group(function () { Route::get('forums/recent-posts', [ForumTopicController::class, 'recentPosts'])->name('forum.recent-posts'); + Route::get('user/{user}/posts', [UserForumTopicCommentController::class, 'index'])->name('user.posts.index'); + Route::get('settings', [UserSettingsController::class, 'show'])->name('settings.show'); }); diff --git a/app/Community/Services/ForumRecentPostsPageService.php b/app/Community/Services/ForumRecentPostsPageService.php deleted file mode 100644 index 42cdc88d6b..0000000000 --- a/app/Community/Services/ForumRecentPostsPageService.php +++ /dev/null @@ -1,99 +0,0 @@ -buildPostsList( - $currentUser, - $currentOffset, - $maxPerPage, - $targetUser, - ); - - [$previousPageUrl, $nextPageUrl] = $this->buildPaginationUrls( - $maxPerPage, - $currentOffset, - count($recentForumPosts), - $targetUser, - ); - - return [ - 'maxPerPage' => $maxPerPage, - 'nextPageUrl' => $nextPageUrl, - 'previousPageUrl' => $previousPageUrl, - 'recentForumPosts' => $recentForumPosts, - 'targetUser' => $targetUser, - ]; - } - - private function buildPaginationUrls( - int $maxPerPage, - int $currentOffset, - int $recentForumPostsCount, - ?User $targetUser = null, - ): array { - $previousPageUrl = null; - $nextPageUrl = null; - - $routeName = $targetUser ? 'user.posts' : 'forum.recent-posts'; - $routeArgs = []; - if ($targetUser) { - $routeArgs['user'] = $targetUser; - } - - if ($currentOffset > 0) { - // Don't let a crawler try to index a URL with "?offset=0". - if ($currentOffset === $maxPerPage) { - $previousPageUrl = route($routeName, $routeArgs); - } else { - $previousPageUrl = route($routeName, [ - ...$routeArgs, - 'offset' => $currentOffset - $maxPerPage, - ]); - } - } - if ($recentForumPostsCount === $maxPerPage) { - $nextPageUrl = route($routeName, [ - ...$routeArgs, - 'offset' => $currentOffset + $maxPerPage, - ]); - } - - return [$previousPageUrl, $nextPageUrl]; - } - - private function buildPostsList( - ?User $currentUser = null, - int $currentOffset = 0, - int $maxPerPage = 25, - ?User $targetUser = null, - ): array { - $postsList = []; - - $currentUserPermissions = $currentUser?->getAttribute('Permissions') ?? Permissions::Unregistered; - - $postsList = getRecentForumPosts( - $currentOffset, - $maxPerPage, - 260, - $currentUserPermissions, - $targetUser->id, - ) - ->toArray(); - - return $postsList; - } -} diff --git a/app/Data/ForumTopicCommentData.php b/app/Data/ForumTopicCommentData.php index 226c9596b1..5d4a287cd5 100644 --- a/app/Data/ForumTopicCommentData.php +++ b/app/Data/ForumTopicCommentData.php @@ -16,7 +16,7 @@ public function __construct( public string $body, public Carbon $createdAt, public ?Carbon $updatedAt, - public UserData $user, + public ?UserData $user, public bool $authorized, // TODO migrate to $authorizedAt public ?int $forumTopicId = null, ) { diff --git a/app/Data/ForumTopicData.php b/app/Data/ForumTopicData.php index 1d1d669bee..f4fe18a6c6 100644 --- a/app/Data/ForumTopicData.php +++ b/app/Data/ForumTopicData.php @@ -18,10 +18,10 @@ public function __construct( public string $title, public Carbon $createdAt, public Lazy|ForumTopicCommentData $latestComment, - public Lazy|int $commentCount24h, - public Lazy|int $oldestComment24hId, - public Lazy|int $commentCount7d, - public Lazy|int $oldestComment7dId, + public Lazy|int|null $commentCount24h, + public Lazy|int|null $oldestComment24hId, + public Lazy|int|null $commentCount7d, + public Lazy|int|null $oldestComment7dId, public ?UserData $user = null, ) { } @@ -54,4 +54,29 @@ public static function fromRecentlyActiveTopic(array $topic): self ); } + + public static function fromUserPost(array $userPost): self + { + return new self( + id: $userPost['ForumTopicID'], + title: $userPost['ForumTopicTitle'], + createdAt: Carbon::parse($userPost['PostedAt']), + + user: null, + + commentCount24h: null, + oldestComment24hId: null, + commentCount7d: null, + oldestComment7dId: null, + + latestComment: Lazy::create(fn () => new ForumTopicCommentData( + id: $userPost['CommentID'], + body: Shortcode::stripAndClamp($userPost['ShortMsg'], 200), + createdAt: Carbon::parse($userPost['PostedAt']), + updatedAt: null, + user: null, + authorized: true + )), + ); + } } diff --git a/app/Models/ForumTopicComment.php b/app/Models/ForumTopicComment.php index 3f3f21a050..15ac9928d8 100644 --- a/app/Models/ForumTopicComment.php +++ b/app/Models/ForumTopicComment.php @@ -4,6 +4,7 @@ namespace App\Models; +use App\Enums\Permissions; use App\Support\Database\Eloquent\BaseModel; use Database\Factories\ForumTopicCommentFactory; use Illuminate\Database\Eloquent\Builder; @@ -145,4 +146,17 @@ public function scopeUnauthorized(Builder $query): Builder ->orWhereNull('authorized_at'); }); } + + /** + * @param Builder $query + * @return Builder + */ + public function scopeViewable(Builder $query, ?User $user = null): Builder + { + $userPermissions = $user ? (int) $user->getAttribute('Permissions') : Permissions::Unregistered; + + return $query->whereHas('forumTopic', function ($query) use ($userPermissions) { + $query->where('RequiredPermissions', '<=', $userPermissions); + }); + } } diff --git a/resources/js/features/forums/components/RecentPostsMainRoot/PostTimestamp/PostTimestamp.test.tsx b/resources/js/common/components/PostTimestamp/PostTimestamp.test.tsx similarity index 100% rename from resources/js/features/forums/components/RecentPostsMainRoot/PostTimestamp/PostTimestamp.test.tsx rename to resources/js/common/components/PostTimestamp/PostTimestamp.test.tsx diff --git a/resources/js/features/forums/components/RecentPostsMainRoot/PostTimestamp/PostTimestamp.tsx b/resources/js/common/components/PostTimestamp/PostTimestamp.tsx similarity index 100% rename from resources/js/features/forums/components/RecentPostsMainRoot/PostTimestamp/PostTimestamp.tsx rename to resources/js/common/components/PostTimestamp/PostTimestamp.tsx diff --git a/resources/js/features/forums/components/RecentPostsMainRoot/PostTimestamp/index.ts b/resources/js/common/components/PostTimestamp/index.ts similarity index 100% rename from resources/js/features/forums/components/RecentPostsMainRoot/PostTimestamp/index.ts rename to resources/js/common/components/PostTimestamp/index.ts diff --git a/resources/js/features/forums/components/RecentPostsMainRoot/AggregateRecentPostLinks/AggregateRecentPostLinks.test.tsx b/resources/js/common/components/RecentPostAggregateLinks/RecentPostAggregateLinks.test.tsx similarity index 86% rename from resources/js/features/forums/components/RecentPostsMainRoot/AggregateRecentPostLinks/AggregateRecentPostLinks.test.tsx rename to resources/js/common/components/RecentPostAggregateLinks/RecentPostAggregateLinks.test.tsx index a958f80cb6..36100c016a 100644 --- a/resources/js/features/forums/components/RecentPostsMainRoot/AggregateRecentPostLinks/AggregateRecentPostLinks.test.tsx +++ b/resources/js/common/components/RecentPostAggregateLinks/RecentPostAggregateLinks.test.tsx @@ -1,13 +1,13 @@ import { render, screen } from '@/test'; import { createRecentActiveForumTopic } from '@/test/factories'; -import { AggregateRecentPostLinks } from './AggregateRecentPostLinks'; +import { RecentPostAggregateLinks } from './RecentPostAggregateLinks'; -describe('Component: AggregateRecentPostLinks', () => { +describe('Component: RecentPostAggregateLinks', () => { it('renders without crashing', () => { // ARRANGE const { container } = render( - , + , ); // ASSERT @@ -17,7 +17,7 @@ describe('Component: AggregateRecentPostLinks', () => { it('given there are not multiple posts in the topic for the day, shows nothing', () => { // ARRANGE render( - { oldestComment7dId: 99999, }); - render(); + render(); // ASSERT const linkEls = screen.getAllByRole('link'); @@ -60,7 +60,7 @@ describe('Component: AggregateRecentPostLinks', () => { oldestComment7dId: 99999, }); - render(); + render(); // ASSERT const linkEls = screen.getAllByRole('link'); @@ -81,7 +81,7 @@ describe('Component: AggregateRecentPostLinks', () => { oldestComment7dId: 99999, }); - render(); + render(); // ASSERT const linkEls = screen.getAllByRole('link'); diff --git a/resources/js/features/forums/components/RecentPostsMainRoot/AggregateRecentPostLinks/AggregateRecentPostLinks.tsx b/resources/js/common/components/RecentPostAggregateLinks/RecentPostAggregateLinks.tsx similarity index 90% rename from resources/js/features/forums/components/RecentPostsMainRoot/AggregateRecentPostLinks/AggregateRecentPostLinks.tsx rename to resources/js/common/components/RecentPostAggregateLinks/RecentPostAggregateLinks.tsx index 2b1998fb87..7e196c2a9f 100644 --- a/resources/js/features/forums/components/RecentPostsMainRoot/AggregateRecentPostLinks/AggregateRecentPostLinks.tsx +++ b/resources/js/common/components/RecentPostAggregateLinks/RecentPostAggregateLinks.tsx @@ -1,10 +1,10 @@ import type { FC } from 'react'; -interface AggregateRecentPostLinksProps { +interface RecentPostAggregateLinksProps { topic: App.Data.ForumTopic; } -export const AggregateRecentPostLinks: FC = ({ topic }) => { +export const RecentPostAggregateLinks: FC = ({ topic }) => { const { commentCount24h, commentCount7d, oldestComment24hId, oldestComment7dId, id } = topic; if (!commentCount7d || commentCount7d <= 1) { diff --git a/resources/js/common/components/RecentPostAggregateLinks/index.ts b/resources/js/common/components/RecentPostAggregateLinks/index.ts new file mode 100644 index 0000000000..811b8acc38 --- /dev/null +++ b/resources/js/common/components/RecentPostAggregateLinks/index.ts @@ -0,0 +1 @@ +export * from './RecentPostAggregateLinks'; diff --git a/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsCards/RecentPostsCards.test.tsx b/resources/js/common/components/RecentPostsCards/RecentPostsCards.test.tsx similarity index 69% rename from resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsCards/RecentPostsCards.test.tsx rename to resources/js/common/components/RecentPostsCards/RecentPostsCards.test.tsx index a300fcb023..ac1c69695b 100644 --- a/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsCards/RecentPostsCards.test.tsx +++ b/resources/js/common/components/RecentPostsCards/RecentPostsCards.test.tsx @@ -6,11 +6,7 @@ import { RecentPostsCards } from './RecentPostsCards'; describe('Component: RecentPostsCards', () => { it('renders without crashing', () => { // ARRANGE - const { container } = render(, { - pageProps: { - paginatedTopics: createPaginatedData([]), - }, - }); + const { container } = render(); // ASSERT expect(container).toBeTruthy(); @@ -18,14 +14,14 @@ describe('Component: RecentPostsCards', () => { it('renders a card for every given recent forum post', () => { // ARRANGE - render(, { - pageProps: { - paginatedTopics: createPaginatedData([ + render( + , + ); // ASSERT expect(screen.getAllByRole('img').length).toEqual(2); // test the presence of user avatars @@ -35,11 +31,7 @@ describe('Component: RecentPostsCards', () => { // ARRANGE const recentActiveForumTopic = createRecentActiveForumTopic(); - render(, { - pageProps: { - paginatedTopics: createPaginatedData([recentActiveForumTopic]), - }, - }); + render(); // ASSERT expect(screen.getByText(recentActiveForumTopic.title)).toBeVisible(); diff --git a/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsCards/RecentPostsCards.tsx b/resources/js/common/components/RecentPostsCards/RecentPostsCards.tsx similarity index 69% rename from resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsCards/RecentPostsCards.tsx rename to resources/js/common/components/RecentPostsCards/RecentPostsCards.tsx index b6292f0c78..23f56254a5 100644 --- a/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsCards/RecentPostsCards.tsx +++ b/resources/js/common/components/RecentPostsCards/RecentPostsCards.tsx @@ -3,11 +3,20 @@ import type { FC } from 'react'; import { UserAvatar } from '@/common/components/UserAvatar'; import { usePageProps } from '@/common/hooks/usePageProps'; -import { AggregateRecentPostLinks } from '../AggregateRecentPostLinks'; import { PostTimestamp } from '../PostTimestamp'; +import { RecentPostAggregateLinks } from '../RecentPostAggregateLinks'; -export const RecentPostsCards: FC = () => { - const { auth, paginatedTopics } = usePageProps(); +interface RecentPostsCardsProps { + paginatedTopics: App.Data.PaginatedData; + + showUser?: boolean; +} + +export const RecentPostsCards: FC = ({ + paginatedTopics, + showUser = true, +}) => { + const { auth } = usePageProps(); return (
@@ -15,7 +24,9 @@ export const RecentPostsCards: FC = () => {
- + {showUser ? ( + + ) : null} {topic.latestComment?.createdAt ? ( @@ -27,7 +38,7 @@ export const RecentPostsCards: FC = () => { ) : null}
- +
diff --git a/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsCards/index.ts b/resources/js/common/components/RecentPostsCards/index.ts similarity index 100% rename from resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsCards/index.ts rename to resources/js/common/components/RecentPostsCards/index.ts diff --git a/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsTable/RecentPostsTable.test.tsx b/resources/js/common/components/RecentPostsTable/RecentPostsTable.test.tsx similarity index 69% rename from resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsTable/RecentPostsTable.test.tsx rename to resources/js/common/components/RecentPostsTable/RecentPostsTable.test.tsx index 52f3631572..c8353be59e 100644 --- a/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsTable/RecentPostsTable.test.tsx +++ b/resources/js/common/components/RecentPostsTable/RecentPostsTable.test.tsx @@ -6,11 +6,7 @@ import { RecentPostsTable } from './RecentPostsTable'; describe('Component: RecentPostsTable', () => { it('renders without crashing', () => { // ARRANGE - const { container } = render(, { - pageProps: { - paginatedTopics: createPaginatedData([]), - }, - }); + const { container } = render(); // ASSERT expect(container).toBeTruthy(); @@ -18,14 +14,14 @@ describe('Component: RecentPostsTable', () => { it('renders a table row for every given recent forum post', () => { // ARRANGE - render(, { - pageProps: { - paginatedTopics: createPaginatedData([ + render( + , + ); // ASSERT expect(screen.getAllByRole('row').length).toEqual(3); // a header row and the two post rows @@ -35,11 +31,7 @@ describe('Component: RecentPostsTable', () => { // ARRANGE const recentActiveForumTopic = createRecentActiveForumTopic(); - render(, { - pageProps: { - paginatedTopics: createPaginatedData([recentActiveForumTopic]), - }, - }); + render(); // ASSERT expect(screen.getByText(recentActiveForumTopic.title)).toBeVisible(); diff --git a/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsTable/RecentPostsTable.tsx b/resources/js/common/components/RecentPostsTable/RecentPostsTable.tsx similarity index 57% rename from resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsTable/RecentPostsTable.tsx rename to resources/js/common/components/RecentPostsTable/RecentPostsTable.tsx index 3144220707..734b99c8c8 100644 --- a/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsTable/RecentPostsTable.tsx +++ b/resources/js/common/components/RecentPostsTable/RecentPostsTable.tsx @@ -3,30 +3,47 @@ import type { FC } from 'react'; import { UserAvatar } from '@/common/components/UserAvatar'; import { usePageProps } from '@/common/hooks/usePageProps'; -import { AggregateRecentPostLinks } from '../AggregateRecentPostLinks'; import { PostTimestamp } from '../PostTimestamp'; +import { RecentPostAggregateLinks } from '../RecentPostAggregateLinks'; -export const RecentPostsTable: FC = () => { - const { auth, paginatedTopics } = usePageProps(); +interface RecentPostsTableProps { + paginatedTopics: App.Data.PaginatedData; + + showAdditionalPosts?: boolean; + showLastPostBy?: boolean; +} + +export const RecentPostsTable: FC = ({ + paginatedTopics, + showAdditionalPosts = true, + showLastPostBy = true, +}) => { + const { auth } = usePageProps(); return ( - + {showLastPostBy ? : null} + - + + {showAdditionalPosts ? ( + + ) : null} {paginatedTopics.items.map((topic) => ( - + {showLastPostBy ? ( + + ) : null} - - + {showAdditionalPosts ? ( + + ) : null} ))} diff --git a/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsTable/index.ts b/resources/js/common/components/RecentPostsTable/index.ts similarity index 100% rename from resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsTable/index.ts rename to resources/js/common/components/RecentPostsTable/index.ts diff --git a/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsPagination/RecentPostsPagination.test.tsx b/resources/js/common/components/SimplePaginator/SimplePaginator.test.tsx similarity index 76% rename from resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsPagination/RecentPostsPagination.test.tsx rename to resources/js/common/components/SimplePaginator/SimplePaginator.test.tsx index ca5fac5eae..35ad6346d8 100644 --- a/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsPagination/RecentPostsPagination.test.tsx +++ b/resources/js/common/components/SimplePaginator/SimplePaginator.test.tsx @@ -3,12 +3,12 @@ import { faker } from '@faker-js/faker'; import { render, screen } from '@/test'; import { createPaginatedData } from '@/test/factories'; -import { RecentPostsPagination } from './RecentPostsPagination'; +import { SimplePaginator } from './SimplePaginator'; -describe('Component: RecentPostsPagination', () => { +describe('Component: SimplePaginator', () => { it('renders without crashing', () => { // ARRANGE - const { container } = render(, { + const { container } = render(, { pageProps: { paginatedTopics: createPaginatedData([]), }, @@ -20,7 +20,7 @@ describe('Component: RecentPostsPagination', () => { it('given there are no pages, renders nothing', () => { // ARRANGE - render(, { + render(, { pageProps: { paginatedTopics: createPaginatedData([]), }, @@ -36,14 +36,14 @@ describe('Component: RecentPostsPagination', () => { const nextPageUrl = faker.internet.url(); const previousPageUrl = faker.internet.url(); - render(, { - pageProps: { - paginatedTopics: createPaginatedData([], { + render( + , + ); // ASSERT const previousLinkEl = screen.getByRole('link', { name: /previous/i }); diff --git a/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsPagination/RecentPostsPagination.tsx b/resources/js/common/components/SimplePaginator/SimplePaginator.tsx similarity index 80% rename from resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsPagination/RecentPostsPagination.tsx rename to resources/js/common/components/SimplePaginator/SimplePaginator.tsx index 9b8eba2191..e6a0370328 100644 --- a/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsPagination/RecentPostsPagination.tsx +++ b/resources/js/common/components/SimplePaginator/SimplePaginator.tsx @@ -7,15 +7,16 @@ import { BasePaginationNext, BasePaginationPrevious, } from '@/common/components/+vendor/BasePagination'; -import { usePageProps } from '@/common/hooks/usePageProps'; -export const RecentPostsPagination: FC = () => { - const { paginatedTopics } = usePageProps(); +interface SimplePaginatorProps { + paginatedData: App.Data.PaginatedData; +} +export const SimplePaginator: FC = ({ paginatedData }) => { const { perPage, links: { nextPageUrl, previousPageUrl }, - } = paginatedTopics; + } = paginatedData; if (!previousPageUrl && !nextPageUrl) { return null; diff --git a/resources/js/common/components/SimplePaginator/index.ts b/resources/js/common/components/SimplePaginator/index.ts new file mode 100644 index 0000000000..1b94747028 --- /dev/null +++ b/resources/js/common/components/SimplePaginator/index.ts @@ -0,0 +1 @@ +export * from './SimplePaginator'; diff --git a/resources/js/common/components/UserAvatar/UserAvatar.tsx b/resources/js/common/components/UserAvatar/UserAvatar.tsx index 5821935519..cab9b1d15a 100644 --- a/resources/js/common/components/UserAvatar/UserAvatar.tsx +++ b/resources/js/common/components/UserAvatar/UserAvatar.tsx @@ -7,10 +7,16 @@ interface UserAvatarProps { displayName: string | null; hasTooltip?: boolean; + showDisplayName?: boolean; size?: AvatarSize; } -export const UserAvatar: FC = ({ displayName, size = 32, hasTooltip = true }) => { +export const UserAvatar: FC = ({ + displayName, + hasTooltip = true, + showDisplayName = true, + size = 32, +}) => { const { cardTooltipProps } = useCardTooltip({ dynamicType: 'user', dynamicId: displayName }); return ( @@ -29,7 +35,7 @@ export const UserAvatar: FC = ({ displayName, size = 32, hasToo className="rounded-sm" /> - {displayName ? {displayName} : null} + {displayName && showDisplayName ? {displayName} : null} ); }; diff --git a/resources/js/features/forums/components/RecentPostsMainRoot/AggregateRecentPostLinks/index.ts b/resources/js/features/forums/components/RecentPostsMainRoot/AggregateRecentPostLinks/index.ts deleted file mode 100644 index f065a37469..0000000000 --- a/resources/js/features/forums/components/RecentPostsMainRoot/AggregateRecentPostLinks/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './AggregateRecentPostLinks'; diff --git a/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsMainRoot.tsx b/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsMainRoot.tsx index ebf1ae32b0..4d9a2d91fd 100644 --- a/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsMainRoot.tsx +++ b/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsMainRoot.tsx @@ -1,11 +1,15 @@ import type { FC } from 'react'; +import { RecentPostsCards } from '@/common/components/RecentPostsCards'; +import { RecentPostsTable } from '@/common/components/RecentPostsTable'; +import { SimplePaginator } from '@/common/components/SimplePaginator'; +import { usePageProps } from '@/common/hooks/usePageProps'; + import { ForumBreadcrumbs } from '../ForumBreadcrumbs'; -import { RecentPostsCards } from './RecentPostsCards'; -import { RecentPostsPagination } from './RecentPostsPagination'; -import { RecentPostsTable } from './RecentPostsTable'; export const RecentPostsMainRoot: FC = () => { + const { paginatedTopics } = usePageProps(); + return (
@@ -13,15 +17,15 @@ export const RecentPostsMainRoot: FC = () => {

Recent Posts

- +
- +
- +
); diff --git a/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsPagination/index.ts b/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsPagination/index.ts deleted file mode 100644 index 294a1ce99a..0000000000 --- a/resources/js/features/forums/components/RecentPostsMainRoot/RecentPostsPagination/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './RecentPostsPagination'; diff --git a/resources/js/features/users/components/UserBreadcrumbs/UserBreadcrumbs.test.tsx b/resources/js/features/users/components/UserBreadcrumbs/UserBreadcrumbs.test.tsx new file mode 100644 index 0000000000..ea986d7328 --- /dev/null +++ b/resources/js/features/users/components/UserBreadcrumbs/UserBreadcrumbs.test.tsx @@ -0,0 +1,36 @@ +import { render, screen } from '@/test'; +import { createUser } from '@/test/factories'; + +import { UserBreadcrumbs } from './UserBreadcrumbs'; + +describe('Component: UserBreadcrumbs', () => { + it('renders without crashing', () => { + // ARRANGE + const { container } = render(); + + // ASSERT + expect(container).toBeTruthy(); + }); + + it('has a link to the All Users list', () => { + // ARRANGE + render(); + + // ASSERT + const allGamesLinkEl = screen.getByRole('link', { name: /all users/i }); + expect(allGamesLinkEl).toBeVisible(); + expect(allGamesLinkEl).toHaveAttribute('href', '/userList.php'); + }); + + it('given a user, has a link to the user profile', () => { + // ARRANGE + const user = createUser({ displayName: 'Scott' }); + + render(); + + // ASSERT + const systemGamesLinkEl = screen.getByRole('link', { name: /scott/i }); + expect(systemGamesLinkEl).toBeVisible(); + expect(systemGamesLinkEl).toHaveAttribute('href', `user.show,${{ user: user.displayName }}`); + }); +}); diff --git a/resources/js/features/users/components/UserBreadcrumbs/UserBreadcrumbs.tsx b/resources/js/features/users/components/UserBreadcrumbs/UserBreadcrumbs.tsx new file mode 100644 index 0000000000..2a6d4459c9 --- /dev/null +++ b/resources/js/features/users/components/UserBreadcrumbs/UserBreadcrumbs.tsx @@ -0,0 +1,48 @@ +import type { FC } from 'react'; + +import { + BaseBreadcrumb, + BaseBreadcrumbItem, + BaseBreadcrumbLink, + BaseBreadcrumbList, + BaseBreadcrumbPage, + BaseBreadcrumbSeparator, +} from '@/common/components/+vendor/BaseBreadcrumb'; + +interface UserBreadcrumbsProps { + currentPageLabel: string; + + user?: App.Data.User; +} + +export const UserBreadcrumbs: FC = ({ currentPageLabel, user }) => { + return ( +
+ + + + All Users + + + {user ? ( + <> + + + + + {user.displayName} + + + + ) : null} + + + + + {currentPageLabel} + + + +
+ ); +}; diff --git a/resources/js/features/users/components/UserBreadcrumbs/index.ts b/resources/js/features/users/components/UserBreadcrumbs/index.ts new file mode 100644 index 0000000000..4a801e57ae --- /dev/null +++ b/resources/js/features/users/components/UserBreadcrumbs/index.ts @@ -0,0 +1 @@ +export * from './UserBreadcrumbs'; diff --git a/resources/js/features/users/components/UserHeading/UserHeading.test.tsx b/resources/js/features/users/components/UserHeading/UserHeading.test.tsx new file mode 100644 index 0000000000..5975d31552 --- /dev/null +++ b/resources/js/features/users/components/UserHeading/UserHeading.test.tsx @@ -0,0 +1,38 @@ +import { render, screen } from '@/test'; +import { createUser } from '@/test/factories'; + +import { UserHeading } from './UserHeading'; + +describe('Component: UserHeading', () => { + it('renders without crashing', () => { + // ARRANGE + const { container } = render(Hello, World); + + // ASSERT + expect(container).toBeTruthy(); + }); + + it('displays a clickable avatar of the given user', () => { + // ARRANGE + const user = createUser(); + + render(Hello, World); + + // ASSERT + const linkEl = screen.getByRole('link'); + expect(linkEl).toBeVisible(); + expect(linkEl).toHaveAttribute('href', `user.show,${user.displayName}`); + + expect(screen.getByRole('img', { name: user.displayName })).toBeVisible(); + }); + + it('displays an accessible header from `children`', () => { + // ARRANGE + const user = createUser(); + + render(Hello, World); + + // ASSERT + expect(screen.getByRole('heading', { name: /hello, world/i })).toBeVisible(); + }); +}); diff --git a/resources/js/features/users/components/UserHeading/UserHeading.tsx b/resources/js/features/users/components/UserHeading/UserHeading.tsx new file mode 100644 index 0000000000..b145ef50d0 --- /dev/null +++ b/resources/js/features/users/components/UserHeading/UserHeading.tsx @@ -0,0 +1,20 @@ +import type { FC, ReactNode } from 'react'; + +import { UserAvatar } from '@/common/components/UserAvatar'; + +interface UserHeadingProps { + children: ReactNode; + user: App.Data.User; +} + +export const UserHeading: FC = ({ children, user }) => { + return ( +
+
+ +
+ +

{children}

+
+ ); +}; diff --git a/resources/js/features/users/components/UserHeading/index.ts b/resources/js/features/users/components/UserHeading/index.ts new file mode 100644 index 0000000000..f45a09bc68 --- /dev/null +++ b/resources/js/features/users/components/UserHeading/index.ts @@ -0,0 +1 @@ +export * from './UserHeading'; diff --git a/resources/js/features/users/components/UserPostsMainRoot/UserPostsMainRoot.tsx b/resources/js/features/users/components/UserPostsMainRoot/UserPostsMainRoot.tsx new file mode 100644 index 0000000000..b664f5a869 --- /dev/null +++ b/resources/js/features/users/components/UserPostsMainRoot/UserPostsMainRoot.tsx @@ -0,0 +1,37 @@ +import type { FC } from 'react'; + +import { RecentPostsCards } from '@/common/components/RecentPostsCards'; +import { RecentPostsTable } from '@/common/components/RecentPostsTable'; +import { SimplePaginator } from '@/common/components/SimplePaginator'; +import { usePageProps } from '@/common/hooks/usePageProps'; + +import { UserBreadcrumbs } from '../UserBreadcrumbs'; +import { UserHeading } from '../UserHeading'; + +export const UserPostsMainRoot: FC = () => { + const { targetUser, paginatedTopics } = + usePageProps(); + + return ( +
+ + {targetUser.displayName}'s Forum Posts + +
+ +
+ +
+ +
+ +
+ +
+
+ ); +}; diff --git a/resources/js/features/users/components/UserPostsMainRoot/index.ts b/resources/js/features/users/components/UserPostsMainRoot/index.ts new file mode 100644 index 0000000000..e92d064a54 --- /dev/null +++ b/resources/js/features/users/components/UserPostsMainRoot/index.ts @@ -0,0 +1 @@ +export * from './UserPostsMainRoot'; diff --git a/resources/js/pages/user/[user]/posts.tsx b/resources/js/pages/user/[user]/posts.tsx new file mode 100644 index 0000000000..4a5462634b --- /dev/null +++ b/resources/js/pages/user/[user]/posts.tsx @@ -0,0 +1,29 @@ +import { Head } from '@inertiajs/react'; + +import { usePageProps } from '@/common/hooks/usePageProps'; +import { AppLayout } from '@/common/layouts/AppLayout'; +import type { AppPage } from '@/common/models'; +import { UserPostsMainRoot } from '@/features/users/components/UserPostsMainRoot'; + +const UserPosts: AppPage = () => { + const { targetUser } = usePageProps(); + + return ( + <> + + + + + + + + + ); +}; + +UserPosts.layout = (page) => {page}; + +export default UserPosts; diff --git a/resources/js/types/generated.d.ts b/resources/js/types/generated.d.ts index 97bb5dd84a..129c948e77 100644 --- a/resources/js/types/generated.d.ts +++ b/resources/js/types/generated.d.ts @@ -2,6 +2,10 @@ declare namespace App.Community.Data { export type RecentPostsPageProps = { paginatedTopics: App.Data.PaginatedData; }; + export type UserRecentPostsPageProps = { + targetUser: App.Data.User; + paginatedTopics: App.Data.PaginatedData; + }; export type UserSettingsPageProps = { userSettings: App.Data.User; can: App.Data.UserPermissions; @@ -13,7 +17,7 @@ declare namespace App.Data { body: string; createdAt: string; updatedAt: string | null; - user: App.Data.User; + user: App.Data.User | null; authorized: boolean; forumTopicId: number | null; }; @@ -22,10 +26,10 @@ declare namespace App.Data { title: string; createdAt: string; latestComment?: App.Data.ForumTopicComment; - commentCount24h?: number; - oldestComment24hId?: number; - commentCount7d?: number; - oldestComment7dId?: number; + commentCount24h?: number | null; + oldestComment24hId?: number | null; + commentCount7d?: number | null; + oldestComment7dId?: number | null; user: App.Data.User | null; }; export type PaginatedData = { diff --git a/resources/js/ziggy.d.ts b/resources/js/ziggy.d.ts index 0e3ab825a3..6bf53615c2 100644 --- a/resources/js/ziggy.d.ts +++ b/resources/js/ziggy.d.ts @@ -106,13 +106,6 @@ declare module 'ziggy-js' { "binding": "User" } ], - "user.posts": [ - { - "name": "user", - "required": true, - "binding": "User" - } - ], "user.game.activity": [ { "name": "user", @@ -300,6 +293,13 @@ declare module 'ziggy-js' { } ], "forum.recent-posts": [], + "user.posts.index": [ + { + "name": "user", + "required": true, + "binding": "User" + } + ], "settings.show": [], "user.comment.destroyAll": [ { diff --git a/resources/views/components/forum/recent-posts/aggregate-recent-posts-links.blade.php b/resources/views/components/forum/recent-posts/aggregate-recent-posts-links.blade.php deleted file mode 100644 index f53acc3eb4..0000000000 --- a/resources/views/components/forum/recent-posts/aggregate-recent-posts-links.blade.php +++ /dev/null @@ -1,25 +0,0 @@ -@props([ - 'count1d' => 0, - 'count7d' => 0, - 'commentId1d' => 0, - 'commentId7d' => 0, - 'forumTopicId' => 0, -]) - -@if ($count7d > 1) -
-
- @if ($count1d > 1) - - {{ $count1d }} posts in the last 24 hours - - @endif - - @if ($count7d > $count1d) - - {{ $count7d }} posts in the last 7 days - - @endif -
-
-@endif diff --git a/resources/views/components/forum/recent-posts/desktop-table.blade.php b/resources/views/components/forum/recent-posts/desktop-table.blade.php deleted file mode 100644 index 0f29f72c58..0000000000 --- a/resources/views/components/forum/recent-posts/desktop-table.blade.php +++ /dev/null @@ -1,71 +0,0 @@ - - -@props([ - 'isForSpecificUser' => false, - 'recentForumPosts' => [], -]) - -
Last Post ByLast Post ByMessageAdditional PostsAdditional Posts
- - + + +

{

- - + +
- - - @if (!$isForSpecificUser) - - @endif - - - - @if (!$isForSpecificUser) - - @endif - - - - - @foreach ($recentForumPosts as $post) - - @if (!$isForSpecificUser) - - @endif - - - - @if (!$isForSpecificUser) - - @endif - - @endforeach - -
Last Post ByMessageAdditional Posts
- {!! userAvatar($post['AuthorDisplayName'] ?? $post['Author'], iconSize: 24, iconClass: 'rounded-sm mr-1') !!} - -

- - {{ $post['ForumTopicTitle'] }} - - - - - - -

-

- {{ Shortcode::stripAndClamp($post['ShortMsg'], 999) }} -

-
-

-
- -
diff --git a/resources/views/components/forum/recent-posts/mobile-cards.blade.php b/resources/views/components/forum/recent-posts/mobile-cards.blade.php deleted file mode 100644 index 46e2bf068b..0000000000 --- a/resources/views/components/forum/recent-posts/mobile-cards.blade.php +++ /dev/null @@ -1,51 +0,0 @@ - - -@props([ - 'isForSpecificUser' => false, - 'recentForumPosts' => [], -]) - -
- @foreach ($recentForumPosts as $post) -
-
-
- {!! userAvatar($post['AuthorDisplayName'] ?? $post['Author'], iconSize: 16, iconClass: 'rounded-sm mr-1') !!} - - - -
- - @if (!$isForSpecificUser) - - @endif -
- -
-

- in - - {{ $post['ForumTopicTitle'] }} - -

- -

- {{ Shortcode::stripAndClamp($post['ShortMsg'], 999) }} -

-
-
- @endforeach -
diff --git a/resources/views/components/forum/recent-posts/paginator.blade.php b/resources/views/components/forum/recent-posts/paginator.blade.php deleted file mode 100644 index 5e09d4305e..0000000000 --- a/resources/views/components/forum/recent-posts/paginator.blade.php +++ /dev/null @@ -1,15 +0,0 @@ -@props([ - 'maxPerPage' => 25, - 'nextPageUrl' => null, // ?string - 'previousPageUrl' => null, // ?string -]) - -
- @if ($previousPageUrl) - < Previous {{ $maxPerPage }} - @endif - - @if ($nextPageUrl) - Next {{ $maxPerPage }} > - @endif -
diff --git a/resources/views/pages/user/[user]/posts.blade.php b/resources/views/pages/user/[user]/posts.blade.php deleted file mode 100644 index 78b5eb7d8b..0000000000 --- a/resources/views/pages/user/[user]/posts.blade.php +++ /dev/null @@ -1,79 +0,0 @@ -can('viewUserPosts', [\App\Models\ForumTopicComment::class, $user])) { - // Traditionally this would probably be a 401. However, - // a 401 might imply the target user has blocked the current user. - // Returning a 404 is a bit less presumptuous. - abort(404); - } - - return $view->with( - $pageService->buildViewData( - $currentUser, - request()->input('offset', 0), - $user, - ) - ); -}); - -?> - -@props([ - 'maxPerPage' => 25, - 'nextPageUrl' => null, // ?string - 'previousPageUrl' => null, // ?string - 'recentForumPosts' => [], - 'targetUser' => null, // User -]) - - - - -
- {!! userAvatar($targetUser->display_name, label: false, iconSize: 48, iconClass: 'rounded-sm') !!} -

{{ $targetUser->display_name }}'s Recent Posts

-
- -
- -
- - - - @if ($previousPageUrl || $nextPageUrl) - - @endif -