From ae00286ec65b0a8ed3ba744360e19d86c0aab63c Mon Sep 17 00:00:00 2001 From: Gerald Leban Date: Thu, 8 Oct 2026 14:27:21 +0200 Subject: [PATCH] feat: redesign the pop showing kit with stub likes and comments --- .adonisjs/client/registry/index.ts | 6 + .adonisjs/client/registry/schema.d.ts | 12 ++ .adonisjs/client/registry/tree.d.ts | 1 + .adonisjs/server/controllers.ts | 1 + .adonisjs/server/pages.d.ts | 1 - .adonisjs/server/routes.d.ts | 3 + app/auth/controllers/callback_controller.ts | 5 +- app/kits/transformers/kit_transformer.ts | 4 + .../controllers/show_kit_controller.ts | 40 ++++++ app/profile/routes.ts | 3 +- database/schema.ts | 30 ++++- database/seeders/sync_user_records_seeder.ts | 4 +- inertia/components/kit_detail_actions.tsx | 58 ++++++++ inertia/components/kit_detail_panel.tsx | 124 ++++++++++++++++++ inertia/components/shelf_kit_detail.tsx | 45 ------- inertia/pages/explore/index.tsx | 8 +- inertia/pages/explore/kit_detail.tsx | 57 -------- inertia/pages/profile.tsx | 10 +- tests/functional/explore/render.spec.ts | 30 ++++- tests/functional/profile/show_kit.spec.ts | 74 +++++++++++ 20 files changed, 395 insertions(+), 121 deletions(-) create mode 100644 app/profile/controllers/show_kit_controller.ts create mode 100644 inertia/components/kit_detail_actions.tsx create mode 100644 inertia/components/kit_detail_panel.tsx delete mode 100644 inertia/components/shelf_kit_detail.tsx delete mode 100644 inertia/pages/explore/kit_detail.tsx create mode 100644 tests/functional/profile/show_kit.spec.ts diff --git a/.adonisjs/client/registry/index.ts b/.adonisjs/client/registry/index.ts index eabf20e..442cbd8 100644 --- a/.adonisjs/client/registry/index.ts +++ b/.adonisjs/client/registry/index.ts @@ -84,6 +84,12 @@ const routes = { tokens: [{"old":"/profile/:handle","type":0,"val":"profile","end":""},{"old":"/profile/:handle","type":1,"val":"handle","end":""}], types: placeholder as Registry['profile']['types'], }, + 'kit': { + methods: ["GET","HEAD"], + pattern: '/profile/:handle/kit/:rkey', + tokens: [{"old":"/profile/:handle/kit/:rkey","type":0,"val":"profile","end":""},{"old":"/profile/:handle/kit/:rkey","type":1,"val":"handle","end":""},{"old":"/profile/:handle/kit/:rkey","type":0,"val":"kit","end":""},{"old":"/profile/:handle/kit/:rkey","type":1,"val":"rkey","end":""}], + types: placeholder as Registry['kit']['types'], + }, 'stats': { methods: ["GET","HEAD"], pattern: '/stats', diff --git a/.adonisjs/client/registry/schema.d.ts b/.adonisjs/client/registry/schema.d.ts index f5791d8..01cbcbc 100644 --- a/.adonisjs/client/registry/schema.d.ts +++ b/.adonisjs/client/registry/schema.d.ts @@ -163,6 +163,18 @@ export interface Registry { errorResponse: ExtractErrorResponse>> } } + 'kit': { + methods: ["GET","HEAD"] + pattern: '/profile/:handle/kit/:rkey' + types: { + body: {} + paramsTuple: [ParamValue, ParamValue] + params: { handle: ParamValue; rkey: ParamValue } + query: {} + response: ExtractResponse>> + errorResponse: ExtractErrorResponse>> + } + } 'stats': { methods: ["GET","HEAD"] pattern: '/stats' diff --git a/.adonisjs/client/registry/tree.d.ts b/.adonisjs/client/registry/tree.d.ts index 8b2f344..3a9476e 100644 --- a/.adonisjs/client/registry/tree.d.ts +++ b/.adonisjs/client/registry/tree.d.ts @@ -19,6 +19,7 @@ export interface ApiDefinition { destroy: typeof routes['kits.destroy'] } profile: typeof routes['profile'] + kit: typeof routes['kit'] stats: typeof routes['stats'] explore: typeof routes['explore'] eventStream: typeof routes['event_stream'] diff --git a/.adonisjs/server/controllers.ts b/.adonisjs/server/controllers.ts index 8dc484b..35b7c93 100644 --- a/.adonisjs/server/controllers.ts +++ b/.adonisjs/server/controllers.ts @@ -23,6 +23,7 @@ export const controllers = { UpdateKit: () => import('#app/kits/controllers/update_kit_controller'), }, profile: { + ShowKit: () => import('#app/profile/controllers/show_kit_controller'), ShowProfile: () => import('#app/profile/controllers/show_profile_controller'), }, stats: { diff --git a/.adonisjs/server/pages.d.ts b/.adonisjs/server/pages.d.ts index 59147cf..2ad527b 100644 --- a/.adonisjs/server/pages.d.ts +++ b/.adonisjs/server/pages.d.ts @@ -16,7 +16,6 @@ declare module '@adonisjs/inertia/types' { 'errors/not_found': ExtractProps<(typeof import('../../inertia/pages/errors/not_found.tsx'))['default']> 'errors/server_error': ExtractProps<(typeof import('../../inertia/pages/errors/server_error.tsx'))['default']> 'explore/index': ExtractProps<(typeof import('../../inertia/pages/explore/index.tsx'))['default']> - 'explore/kit_detail': ExtractProps<(typeof import('../../inertia/pages/explore/kit_detail.tsx'))['default']> 'home/activity_ticker': ExtractProps<(typeof import('../../inertia/pages/home/activity_ticker.tsx'))['default']> 'home/hero': ExtractProps<(typeof import('../../inertia/pages/home/hero.tsx'))['default']> 'home/index': ExtractProps<(typeof import('../../inertia/pages/home/index.tsx'))['default']> diff --git a/.adonisjs/server/routes.d.ts b/.adonisjs/server/routes.d.ts index 6b5a78e..7251d4a 100644 --- a/.adonisjs/server/routes.d.ts +++ b/.adonisjs/server/routes.d.ts @@ -17,6 +17,7 @@ export type ScannedRoutes = { 'kits.update': { paramsTuple: [ParamValue]; params: {'rkey': ParamValue} } 'kits.destroy': { paramsTuple: [ParamValue]; params: {'rkey': ParamValue} } 'profile': { paramsTuple: [ParamValue]; params: {'handle': ParamValue} } + 'kit': { paramsTuple: [ParamValue,ParamValue]; params: {'handle': ParamValue,'rkey': ParamValue} } 'stats': { paramsTuple?: []; params?: {} } 'explore': { paramsTuple?: []; params?: {} } 'event_stream': { paramsTuple?: []; params?: {} } @@ -31,6 +32,7 @@ export type ScannedRoutes = { 'kits.new': { paramsTuple?: []; params?: {} } 'kits.edit': { paramsTuple: [ParamValue]; params: {'rkey': ParamValue} } 'profile': { paramsTuple: [ParamValue]; params: {'handle': ParamValue} } + 'kit': { paramsTuple: [ParamValue,ParamValue]; params: {'handle': ParamValue,'rkey': ParamValue} } 'stats': { paramsTuple?: []; params?: {} } 'explore': { paramsTuple?: []; params?: {} } 'event_stream': { paramsTuple?: []; params?: {} } @@ -43,6 +45,7 @@ export type ScannedRoutes = { 'kits.new': { paramsTuple?: []; params?: {} } 'kits.edit': { paramsTuple: [ParamValue]; params: {'rkey': ParamValue} } 'profile': { paramsTuple: [ParamValue]; params: {'handle': ParamValue} } + 'kit': { paramsTuple: [ParamValue,ParamValue]; params: {'handle': ParamValue,'rkey': ParamValue} } 'stats': { paramsTuple?: []; params?: {} } 'explore': { paramsTuple?: []; params?: {} } 'event_stream': { paramsTuple?: []; params?: {} } diff --git a/app/auth/controllers/callback_controller.ts b/app/auth/controllers/callback_controller.ts index 78b7acc..e2c2805 100644 --- a/app/auth/controllers/callback_controller.ts +++ b/app/auth/controllers/callback_controller.ts @@ -32,10 +32,7 @@ export default class CallbackController { this.repoSyncService .syncUser(did) .then(({ handle, kitsSynced }) => { - logger.info( - { did, handle, kitsSynced }, - `PDS sync completed on login for ${handle}` - ) + logger.info({ did, handle, kitsSynced }, `PDS sync completed on login for ${handle}`) }) .catch((err) => { logger.warn({ err, did }, `PDS sync failed on login for ${did}`) diff --git a/app/kits/transformers/kit_transformer.ts b/app/kits/transformers/kit_transformer.ts index f336756..e8b4c95 100644 --- a/app/kits/transformers/kit_transformer.ts +++ b/app/kits/transformers/kit_transformer.ts @@ -64,6 +64,7 @@ export default class KitTransformer extends BaseTransformer { forExplore() { const did = this.resource.did + const rkey = new AtUri(this.resource.uri).rkey const photoCidList: string[] = [] if (Array.isArray(this.resource.photos) && this.resource.photos.length > 0) { @@ -85,12 +86,15 @@ export default class KitTransformer extends BaseTransformer { return { id: this.resource.id, uri: this.resource.uri, + rkey, handle, author: ProfileTransformer.transform(this.whenLoaded(this.resource.profile)), name: this.resource.name, manufacturer: this.resource.manufacturer, grade: this.resource.grade, scale: this.resource.scale, + status: this.resource.status, + acquiredAt: this.resource.acquiredAt ? this.resource.acquiredAt.toISO() : null, photos, } } diff --git a/app/profile/controllers/show_kit_controller.ts b/app/profile/controllers/show_kit_controller.ts new file mode 100644 index 0000000..9129eb9 --- /dev/null +++ b/app/profile/controllers/show_kit_controller.ts @@ -0,0 +1,40 @@ +import { inject } from '@adonisjs/core' +import type { HttpContext } from '@adonisjs/core/http' +import { AtUri } from '@atproto/syntax' +import { ProfileService } from '#profile/services/profile_service' +import KitTransformer from '#kits/transformers/kit_transformer' + +@inject() +export default class ShowKitController { + constructor(private profileService: ProfileService) {} + + async render({ params, inertia, response }: HttpContext) { + const handle = params.handle as string + const rkey = params.rkey as string + const profile = await this.profileService.getProfileData(handle) + + if (!profile) { + response.status(404) + return inertia.render('errors/not_found', { handle }) + } + + const kit = profile.kits.find((k) => new AtUri(k.uri).rkey === rkey) + + if (!kit) { + response.status(404) + return inertia.render('errors/not_found', { handle }) + } + + return inertia.render('profile', { + handle: profile.handle, + did: profile.did, + avatarUrl: profile.avatarUrl, + description: profile.description, + displayName: profile.displayName, + kits: KitTransformer.transform(profile.kits).useVariant('forProfileShelf'), + stats: profile.stats, + achievements: profile.achievements, + selectedRkey: rkey, + }) + } +} diff --git a/app/profile/routes.ts b/app/profile/routes.ts index 421564c..65ef816 100644 --- a/app/profile/routes.ts +++ b/app/profile/routes.ts @@ -2,7 +2,8 @@ import router from '@adonisjs/core/services/router' import { controllers } from '#generated/controllers' const { - profile: { ShowProfile: ShowProfileController }, + profile: { ShowProfile: ShowProfileController, ShowKit: ShowKitController }, } = controllers router.get('/profile/:handle', [ShowProfileController, 'render']).as('profile') +router.get('/profile/:handle/kit/:rkey', [ShowKitController, 'render']).as('kit') diff --git a/database/schema.ts b/database/schema.ts index 3ffc016..6fab760 100644 --- a/database/schema.ts +++ b/database/schema.ts @@ -8,7 +8,24 @@ import { BaseModel, column } from '@adonisjs/lucid/orm' import { DateTime } from 'luxon' export class KitSchema extends BaseModel { - static $columns = ['acquiredAt', 'cid', 'createdAt', 'did', 'grade', 'id', 'indexedAt', 'manufacturer', 'name', 'photoCid', 'photos', 'recordCreatedAt', 'scale', 'status', 'updatedAt', 'uri'] as const + static $columns = [ + 'acquiredAt', + 'cid', + 'createdAt', + 'did', + 'grade', + 'id', + 'indexedAt', + 'manufacturer', + 'name', + 'photoCid', + 'photos', + 'recordCreatedAt', + 'scale', + 'status', + 'updatedAt', + 'uri', + ] as const $columns = KitSchema.$columns @column.dateTime() declare acquiredAt: DateTime | null @@ -67,7 +84,16 @@ export class OauthStateSchema extends BaseModel { } export class ProfileSchema extends BaseModel { - static $columns = ['avatarUrl', 'createdAt', 'description', 'did', 'displayName', 'handle', 'indexedAt', 'updatedAt'] as const + static $columns = [ + 'avatarUrl', + 'createdAt', + 'description', + 'did', + 'displayName', + 'handle', + 'indexedAt', + 'updatedAt', + ] as const $columns = ProfileSchema.$columns @column() declare avatarUrl: string | null diff --git a/database/seeders/sync_user_records_seeder.ts b/database/seeders/sync_user_records_seeder.ts index 58651f0..58c221c 100644 --- a/database/seeders/sync_user_records_seeder.ts +++ b/database/seeders/sync_user_records_seeder.ts @@ -16,8 +16,6 @@ export default class extends BaseSeeder { const syncService = await app.container.make(RepoSyncService) const result = await syncService.syncUser(syncUser) - logger.info( - `Synced @${result.handle} (${result.kitsSynced} kits) from ${result.pdsEndpoint}` - ) + logger.info(`Synced @${result.handle} (${result.kitsSynced} kits) from ${result.pdsEndpoint}`) } } diff --git a/inertia/components/kit_detail_actions.tsx b/inertia/components/kit_detail_actions.tsx new file mode 100644 index 0000000..d88d1e6 --- /dev/null +++ b/inertia/components/kit_detail_actions.tsx @@ -0,0 +1,58 @@ +import { Heart, MessageCircle, Link2 } from 'lucide-react' +import { toast } from 'sonner' + +interface KitDetailActionsProps { + handle: string | null + rkey: string +} + +export function KitDetailActions({ handle, rkey }: KitDetailActionsProps) { + function handleShare() { + const url = window.location.origin + '/profile/' + handle + '/kit/' + rkey + navigator.clipboard.writeText(url).then(() => { + toast.success('Link copied') + }) + } + + return ( +
+
+ + + +
+ +

0 likes

+ +
+ +
+
+ ) +} diff --git a/inertia/components/kit_detail_panel.tsx b/inertia/components/kit_detail_panel.tsx new file mode 100644 index 0000000..816109a --- /dev/null +++ b/inertia/components/kit_detail_panel.tsx @@ -0,0 +1,124 @@ +import { X } from 'lucide-react' +import { Link } from '@adonisjs/inertia/react' +import { Avatar, AvatarImage, AvatarFallback } from '~/components/ui/avatar' +import { DialogClose } from '~/components/ui/dialog' +import { PhotoCarousel } from '~/components/photo_carousel' +import { StatusBadge } from '~/components/kit_preview' +import { KitDetailActions } from '~/components/kit_detail_actions' +import type { PhotoItem } from '#kits/transformers/kit_transformer' + +interface KitForPanel { + handle: string | null + author?: { + avatarUrl: string | null + displayName: string | null + handle: string | null + } | null + name: string + grade: string | null + scale: string | null + manufacturer: string | null + status: string + acquiredAt: string | null + photos: PhotoItem[] + rkey: string +} + +interface KitDetailPanelProps { + kit: KitForPanel + onClose: () => void +} + +function formatDate(iso: string) { + return new Date(iso).toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + }) +} + +export function KitDetailPanel({ kit, onClose }: KitDetailPanelProps) { + const handle = kit.author?.handle ?? kit.handle + const displayName = kit.author?.displayName + const avatarUrl = kit.author?.avatarUrl + const shortHandle = (handle ?? '').split('.')[0] + + return ( +
+ {/* Left — carousel */} +
+ +
+ + {/* Right — info panel */} +
+ {/* Author row */} +
+ + + {avatarUrl && } + {shortHandle.slice(0, 2).toUpperCase()} + +
+ {displayName && ( +

+ {displayName} +

+ )} +

@{shortHandle}

+
+ +
+ + {/* Kit header */} +
+
+ {[kit.grade, kit.scale].filter(Boolean).join(' · ') && ( +
+ + {[kit.grade, kit.scale].filter(Boolean).join(' · ')} + +
+ )} +

{kit.name}

+ {kit.manufacturer && ( +

{kit.manufacturer}

+ )} +
+ + + +
+ + {/* Scrollable body */} +
+
+ + {kit.acquiredAt && ( + + Acquired {formatDate(kit.acquiredAt)} + + )} +
+ +

No comments yet

+
+ + {/* Actions — anchored to bottom */} + +
+
+ ) +} diff --git a/inertia/components/shelf_kit_detail.tsx b/inertia/components/shelf_kit_detail.tsx deleted file mode 100644 index 9eec426..0000000 --- a/inertia/components/shelf_kit_detail.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { X } from 'lucide-react' -import { DialogClose } from '~/components/ui/dialog' -import { PhotoCarousel } from '~/components/photo_carousel' -import type { InferVariants } from '@adonisjs/http-transformers/types' -import type KitTransformer from '#kits/transformers/kit_transformer' - -type KitShelf = InferVariants['forProfileShelf'] - -export function ShelfKitDetail({ kit, onClose }: { kit: KitShelf; onClose: () => void }) { - return ( -
-
- -
- -
-
-
- {[kit.grade, kit.scale].filter(Boolean).join(' · ') && ( -
- - {[kit.grade, kit.scale].filter(Boolean).join(' · ')} - -
- )} -

{kit.name}

- {kit.manufacturer && ( -

{kit.manufacturer}

- )} -
- - - -
-
-
- ) -} diff --git a/inertia/pages/explore/index.tsx b/inertia/pages/explore/index.tsx index 81a2e69..71dca26 100644 --- a/inertia/pages/explore/index.tsx +++ b/inertia/pages/explore/index.tsx @@ -15,7 +15,11 @@ import { EmptyContent, } from '~/components/ui/empty' import type { InertiaProps, Paginated } from '~/types' -import { KitDetail, type KitExplore } from './kit_detail' +import type { InferVariants } from '@adonisjs/http-transformers/types' +import type KitTransformer from '#kits/transformers/kit_transformer' +import { KitDetailPanel } from '~/components/kit_detail_panel' + +type KitExplore = InferVariants['forExplore'] type PageProps = InertiaProps<{ kits: Paginated @@ -189,7 +193,7 @@ export default function Explore() { className="max-h-[calc(100svh-2rem)] max-w-[calc(100%-2rem)] sm:max-w-4xl overflow-hidden border-2 border-border p-0 shadow-[6px_6px_0px_0px_hsl(var(--border))]" showCloseButton={false} > - {selected && setSelected(null)} />} + {selected && setSelected(null)} />} diff --git a/inertia/pages/explore/kit_detail.tsx b/inertia/pages/explore/kit_detail.tsx deleted file mode 100644 index b16ae37..0000000 --- a/inertia/pages/explore/kit_detail.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { Link } from '@adonisjs/inertia/react' -import { X } from 'lucide-react' -import { DialogClose } from '~/components/ui/dialog' -import { PhotoCarousel } from '~/components/photo_carousel' -import type { InferVariants } from '@adonisjs/http-transformers/types' -import type KitTransformer from '#kits/transformers/kit_transformer' - -export type KitExplore = InferVariants['forExplore'] - -export function KitDetail({ kit, onClose }: { kit: KitExplore; onClose: () => void }) { - return ( -
- {/* Left — carousel */} -
- -
- - {/* Right — info */} -
- {/* Header */} -
-
- {[kit.grade, kit.scale].filter(Boolean).join(' · ') && ( -
- - {[kit.grade, kit.scale].filter(Boolean).join(' · ')} - -
- )} -

{kit.name}

-

- {kit.manufacturer} - {kit.manufacturer && ' · '} - - @{(kit.handle ?? '').split('.')[0]} - -

-
- - - -
-
-
- ) -} diff --git a/inertia/pages/profile.tsx b/inertia/pages/profile.tsx index 04aede6..ff63b84 100644 --- a/inertia/pages/profile.tsx +++ b/inertia/pages/profile.tsx @@ -3,7 +3,7 @@ import { Link } from '@adonisjs/inertia/react' import { usePage } from '@inertiajs/react' import BaseLayout from '~/layouts/base' import KitCard from '~/components/kit_card' -import { ShelfKitDetail } from '~/components/shelf_kit_detail' +import { KitDetailPanel } from '~/components/kit_detail_panel' import { StatusBadge, STATUS_CONFIG } from '~/components/kit_preview' import { Button } from '~/components/ui/button' import { Progress } from '~/components/ui/progress' @@ -39,6 +39,7 @@ type PageProps = InertiaProps<{ kits: KitShelf[] stats: ProfileStats achievements: Achievement[] + selectedRkey?: string }> export default function Profile({ @@ -50,6 +51,7 @@ export default function Profile({ kits, stats, achievements, + selectedRkey, }: PageProps) { const page = usePage() const viewerDid = page.props.user?.did @@ -58,7 +60,9 @@ export default function Profile({ const [statusFilter, setStatusFilter] = useState('all') const [searchQuery, setSearchQuery] = useState('') const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid') - const [selected, setSelected] = useState(null) + const [selected, setSelected] = useState(() => + selectedRkey ? (kits.find((k) => k.rkey === selectedRkey) ?? null) : null + ) const unlockedCount = achievements ? achievements.filter((a) => a.unlocked).length : 0 @@ -487,7 +491,7 @@ export default function Profile({ className="max-h-[calc(100svh-2rem)] max-w-[calc(100%-2rem)] sm:max-w-3xl overflow-hidden border-2 border-black p-0 shadow-[6px_6px_0px_0px_rgba(0,0,0,1)]" showCloseButton={false} > - {selected && setSelected(null)} />} + {selected && setSelected(null)} />} diff --git a/tests/functional/explore/render.spec.ts b/tests/functional/explore/render.spec.ts index 0abac1e..b2ac9e3 100644 --- a/tests/functional/explore/render.spec.ts +++ b/tests/functional/explore/render.spec.ts @@ -1,14 +1,13 @@ import { test } from '@japa/runner' import testUtils from '@adonisjs/core/services/test_utils' +import { DateTime } from 'luxon' import { KitFactory } from '#database/factories/kit_factory' import { ProfileFactory } from '#database/factories/profile_factory' test.group('ExploreController.render', (group) => { group.each.setup(() => testUtils.db().truncate()) - test('explore renders completed kits with transformer-shaped fields', async ({ - client, - }) => { + test('explore renders completed kits with transformer-shaped fields', async ({ client }) => { await ProfileFactory.merge({ did: 'did:plc:amuro', handle: 'amuro.bsky.social', @@ -131,4 +130,29 @@ test.group('ExploreController.render', (group) => { response.assertInertiaPropsContains({ kits: { data: [] } }) }) + + test('explore kit objects include rkey, status, and acquiredAt', async ({ client, assert }) => { + await ProfileFactory.merge({ + did: 'did:plc:amuro', + handle: 'amuro.bsky.social', + }).create() + + await KitFactory.merge({ + did: 'did:plc:amuro', + status: 'done', + acquiredAt: DateTime.fromISO('2024-06-01'), + }).create() + + const response = await client.visit('explore').withInertia() + + response.assertStatus(200) + response.assertInertiaComponent('explore/index') + + const kit = response.inertiaProps.kits.data[0] + assert.isString(kit.rkey) + assert.isNotEmpty(kit.rkey) + assert.equal(kit.status, 'done') + assert.isString(kit.acquiredAt) + assert.isNotEmpty(kit.acquiredAt) + }) }) diff --git a/tests/functional/profile/show_kit.spec.ts b/tests/functional/profile/show_kit.spec.ts new file mode 100644 index 0000000..f24b811 --- /dev/null +++ b/tests/functional/profile/show_kit.spec.ts @@ -0,0 +1,74 @@ +import { test } from '@japa/runner' +import testUtils from '@adonisjs/core/services/test_utils' +import { KitFactory } from '#database/factories/kit_factory' +import { ProfileFactory } from '#database/factories/profile_factory' +import { IdentityService } from '#profile/services/identity_service' +import { FakeIdentityService } from '#tests/helpers/fake_identity_service' + +test.group('ShowKitController.render', (group) => { + group.each.setup(() => testUtils.db().truncate()) + + test('kit found at dedicated URL renders profile page with selectedRkey', async ({ + client, + swap, + assert, + }) => { + await ProfileFactory.merge({ did: 'did:plc:amuro', handle: 'amuro.bsky.social' }).create() + await KitFactory.merge({ + did: 'did:plc:amuro', + uri: 'at://did:plc:amuro/gunpla.kit/abc123rkey01', + status: 'done', + }).create() + swap( + IdentityService, + new FakeIdentityService({ + resolver: (id) => + id === 'amuro.bsky.social' ? { did: 'did:plc:amuro', handle: 'amuro.bsky.social' } : null, + }) + ) + + const response = await client + .visit('kit', { handle: 'amuro.bsky.social', rkey: 'abc123rkey01' }) + .withInertia() + + response.assertStatus(200) + response.assertInertiaComponent('profile') + response.assertInertiaPropsContains({ + selectedRkey: 'abc123rkey01', + handle: 'amuro.bsky.social', + }) + assert.isAbove((response.inertiaProps.kits as unknown[]).length, 0) + }) + + test('dedicated kit URL for unknown profile returns 404', async ({ client, swap }) => { + swap(IdentityService, new FakeIdentityService({ defaultHandle: null })) + + const response = await client + .visit('kit', { handle: 'nonexistent.bsky.social', rkey: 'abc123rkey01' }) + .withInertia() + + response.assertStatus(404) + response.assertInertiaComponent('errors/not_found') + }) + + test('dedicated kit URL for profile found but rkey absent returns 404', async ({ + client, + swap, + }) => { + await ProfileFactory.merge({ did: 'did:plc:amuro', handle: 'amuro.bsky.social' }).create() + swap( + IdentityService, + new FakeIdentityService({ + resolver: (id) => + id === 'amuro.bsky.social' ? { did: 'did:plc:amuro', handle: 'amuro.bsky.social' } : null, + }) + ) + + const response = await client + .visit('kit', { handle: 'amuro.bsky.social', rkey: 'ghost-rkey-xyz' }) + .withInertia() + + response.assertStatus(404) + response.assertInertiaComponent('errors/not_found') + }) +}) -- 2.54.0