Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
505 changes: 0 additions & 505 deletions duplication-audit.md

This file was deleted.

14 changes: 10 additions & 4 deletions next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const nextConfig: NextConfig = {
],
},

allowedDevOrigins: ['dev.emuready.com'],
allowedDevOrigins: ['dev.emuready.com', '127.0.0.1'],

turbopack: {
rules: {
Expand Down Expand Up @@ -115,6 +115,7 @@ const nextConfig: NextConfig = {
},

async headers() {
const isProduction = process.env.NODE_ENV === 'production'
const headers = [
{
source: '/service-worker.js',
Expand All @@ -124,10 +125,15 @@ const nextConfig: NextConfig = {
source: '/sw-register.js',
headers: [{ key: 'Cache-Control', value: 'no-store, no-cache, must-revalidate' }],
},
// Static assets with hash - cache immutable
// Static assets are immutable in production and uncached in dev.
{
source: '/_next/static/:path*',
headers: [{ key: 'Cache-Control', value: 'public, max-age=31536000, immutable' }],
headers: isProduction
? [{ key: 'Cache-Control', value: 'public, max-age=31536000, immutable' }]
: [
{ key: 'Cache-Control', value: 'no-store, no-cache, must-revalidate' },
{ key: 'Pragma', value: 'no-cache' },
],
},
// Images and other assets - cache with revalidation
{
Expand Down Expand Up @@ -182,7 +188,7 @@ const nextConfig: NextConfig = {
]

// In dev disable HTML caching to avoid stale content via proxies
if (process.env.NODE_ENV !== 'production') {
if (!isProduction) {
headers.push({
// All routes except static assets and API
source: '/((?!_next|api|favicon|service-worker\\.js|sw-register\\.js).*)',
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
"db:seed": "./scripts/db-cmd.sh pnpm exec tsx prisma/seed.ts",
"db:seed:permissions": "./scripts/db-cmd.sh pnpm exec tsx prisma/seed-permissions.ts",
"db:studio": "./scripts/db-cmd.sh pnpm exec prisma studio",
"dev": "next dev --turbopack",
"dev": "next dev",
"dev:turbo": "next dev --turbopack",
"dev:tracing": "NEXT_TURBOPACK_TRACING=1 next dev --turbopack",
"dev:profile": "NEXT_CPU_PROF=1 NEXT_TURBOPACK_TRACING=1 next dev --turbopack",
"dev:debug": "DEBUG=next:* next dev --turbopack",
Expand Down
11 changes: 8 additions & 3 deletions src/app/admin/approvals/components/ApprovalModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
RejectionNotesInput,
CustomFieldsApprovalSection,
} from '@/app/listings/components/shared/approval/ApprovalModalSharedComponents'
import { AuthorRiskWarningBanner, Modal, Button } from '@/components/ui'
import { ReviewRiskWarningBanner, Modal, Button } from '@/components/ui'
import { type RouterOutput } from '@/types/trpc'
import { ApprovalStatus } from '@orm'

Expand All @@ -25,7 +25,9 @@ interface Props {
}

function ApprovalModal(props: Props) {
const hasRisk = props.selectedListingForApproval.authorRiskProfile?.highestSeverity !== null
const hasRisk =
Boolean(props.selectedListingForApproval.authorRiskProfile?.highestSeverity) ||
Boolean(props.selectedListingForApproval.submissionRiskProfile?.highestSeverity)
const actionText = props.approvalDecision === ApprovalStatus.APPROVED ? 'Approve' : 'Reject'
const modalTitle = `${actionText} Listing: ${props.selectedListingForApproval.game.title}`

Expand All @@ -51,7 +53,10 @@ function ApprovalModal(props: Props) {
size="lg"
>
<div className="space-y-4">
<AuthorRiskWarningBanner riskProfile={props.selectedListingForApproval.authorRiskProfile} />
<ReviewRiskWarningBanner
authorRiskProfile={props.selectedListingForApproval.authorRiskProfile}
submissionRiskProfile={props.selectedListingForApproval.submissionRiskProfile}
/>

{/* Listing Details Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
Expand Down
48 changes: 29 additions & 19 deletions src/app/admin/approvals/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,29 @@ import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { useState } from 'react'
import { isEmpty } from 'remeda'
import { useAdminTable } from '@/app/admin/hooks'
import { useAdminTable, useReviewRiskFilter } from '@/app/admin/hooks'
import { confirmBulkApproval } from '@/app/admin/utils'
import {
AdminErrorState,
AdminPageLayout,
AdminTableContainer,
AdminNotificationBanner,
AdminStatsDisplay,
AdminSearchFilters,
AdminTableNoResults,
ReviewRiskFilterButton,
} from '@/components/admin'
import { EmulatorIcon, SystemIcon } from '@/components/icons'
import {
ApproveButton,
AuthorRiskIndicator,
BulkActions,
Button,
ColumnVisibilityControl,
DisplayToggleButton,
LoadingSpinner,
LocalizedDate,
Pagination,
RejectButton,
ReviewRiskIndicator,
SortableHeader,
ViewButton,
Tooltip,
Expand Down Expand Up @@ -90,6 +91,11 @@ function AdminApprovalsPage() {
)

const emulatorLogos = useEmulatorLogos()
const [selectedListingIds, setSelectedListingIds] = useState<string[]>([])
const reviewRiskFilter = useReviewRiskFilter({
clearSelection: () => setSelectedListingIds([]),
resetPage: () => table.setPage(1),
})

const currentUserQuery = api.users.me.useQuery()
const pendingListingsQuery = api.listings.getPending.useQuery({
Expand All @@ -98,6 +104,7 @@ function AdminApprovalsPage() {
sortField: table.sortField ?? null,
sortDirection: table.sortDirection ?? null,
search: isEmpty(table.search) ? null : table.search,
riskFilter: reviewRiskFilter.riskFilter,
})

const gameStatsQuery = api.games.stats.useQuery()
Expand All @@ -108,7 +115,6 @@ function AdminApprovalsPage() {
useState<PendingListing | null>(null)
const [approvalNotes, setApprovalNotes] = useState('')
const [approvalDecision, setApprovalDecision] = useState<ApprovalStatus | null>(null)
const [selectedListingIds, setSelectedListingIds] = useState<string[]>([])
const confirm = useConfirmDialog()

const utils = api.useUtils()
Expand Down Expand Up @@ -256,19 +262,14 @@ function AdminApprovalsPage() {
}
}

// TODO: extract this to a generic Admin error component
if (pendingListingsQuery.error) {
return (
<div className="container mx-auto px-4 py-8">
<div className="text-center py-12">
<p className="text-red-600 dark:text-red-400 text-lg">
Error loading pending listings: {pendingListingsQuery.error.message}
</p>
<Button onClick={() => pendingListingsQuery.refetch()} className="mt-4">
Try Again
</Button>
</div>
</div>
<AdminErrorState
message={`Error loading pending listings: ${pendingListingsQuery.error.message}`}
onRetry={() => {
void pendingListingsQuery.refetch()
}}
/>
)
}

Expand Down Expand Up @@ -338,7 +339,12 @@ function AdminApprovalsPage() {
/>
)}

<AdminSearchFilters<ApprovalSortField> table={table} searchPlaceholder="Search listings..." />
<AdminSearchFilters<ApprovalSortField> table={table} searchPlaceholder="Search listings...">
<ReviewRiskFilterButton
isActive={reviewRiskFilter.isRiskOnly}
onToggle={reviewRiskFilter.toggleRiskFilter}
/>
</AdminSearchFilters>

{/* Bulk Actions */}
{listings.length > 0 && (
Expand Down Expand Up @@ -372,7 +378,10 @@ function AdminApprovalsPage() {
{pendingListingsQuery.isPending ? (
<LoadingSpinner text="Loading pending listings..." />
) : listings.length === 0 ? (
<AdminTableNoResults icon={Clock} hasQuery={!!table.search} />
<AdminTableNoResults
icon={Clock}
hasQuery={!!table.search || reviewRiskFilter.isRiskOnly}
/>
) : (
<div className="overflow-x-auto">
<table className="min-w-full">
Expand Down Expand Up @@ -506,8 +515,9 @@ function AdminApprovalsPage() {
'N/A'
)}
</span>
<AuthorRiskIndicator
riskProfile={listing.authorRiskProfile}
<ReviewRiskIndicator
authorRiskProfile={listing.authorRiskProfile}
submissionRiskProfile={listing.submissionRiskProfile}
size="sm"
onInvestigate={(authorId) =>
router.push(`/admin/users?userId=${authorId}&tab=reports`)
Expand Down
1 change: 1 addition & 0 deletions src/app/admin/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './useAdminTable'
export * from './useReviewRiskFilter'
29 changes: 29 additions & 0 deletions src/app/admin/hooks/useReviewRiskFilter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useState } from 'react'
import { REVIEW_RISK_FILTERS, type ReviewRiskFilter } from '@/schemas/submissionRisk'

interface Options {
clearSelection: () => void
resetPage: () => void
}

interface ReviewRiskFilterState {
isRiskOnly: boolean
riskFilter: ReviewRiskFilter
toggleRiskFilter: () => void
}

export function useReviewRiskFilter(options: Options): ReviewRiskFilterState {
const [isRiskOnly, setIsRiskOnly] = useState(false)

const toggleRiskFilter = () => {
setIsRiskOnly((current) => !current)
options.clearSelection()
options.resetPage()
}

return {
isRiskOnly,
riskFilter: isRiskOnly ? REVIEW_RISK_FILTERS.RISKY : REVIEW_RISK_FILTERS.ALL,
toggleRiskFilter,
}
}
11 changes: 7 additions & 4 deletions src/app/admin/pc-listing-approvals/components/ApprovalModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
RejectionNotesInput,
CustomFieldsApprovalSection,
} from '@/app/listings/components/shared/approval/ApprovalModalSharedComponents'
import { AuthorRiskWarningBanner, Button, Modal } from '@/components/ui'
import { ReviewRiskWarningBanner, Button, Modal } from '@/components/ui'
import { useEmulatorLogos } from '@/hooks'
import { type RouterOutput } from '@/types/trpc'
import { ApprovalStatus } from '@orm'
Expand All @@ -31,7 +31,9 @@ interface Props {
function ApprovalModal(props: Props) {
const emulatorLogos = useEmulatorLogos()

const hasRisk = props.selectedPcListingForApproval.authorRiskProfile?.highestSeverity !== null
const hasRisk =
Boolean(props.selectedPcListingForApproval.authorRiskProfile?.highestSeverity) ||
Boolean(props.selectedPcListingForApproval.submissionRiskProfile?.highestSeverity)

const getModalTitle = () => {
const actionText = props.approvalDecision === ApprovalStatus.APPROVED ? 'Approve' : 'Reject'
Expand All @@ -56,8 +58,9 @@ function ApprovalModal(props: Props) {
size="lg"
>
<div className="space-y-4">
<AuthorRiskWarningBanner
riskProfile={props.selectedPcListingForApproval.authorRiskProfile}
<ReviewRiskWarningBanner
authorRiskProfile={props.selectedPcListingForApproval.authorRiskProfile}
submissionRiskProfile={props.selectedPcListingForApproval.submissionRiskProfile}
/>

<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
Expand Down
47 changes: 29 additions & 18 deletions src/app/admin/pc-listing-approvals/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,29 @@ import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { useState } from 'react'
import { isEmpty } from 'remeda'
import { useAdminTable } from '@/app/admin/hooks'
import { useAdminTable, useReviewRiskFilter } from '@/app/admin/hooks'
import { confirmBulkApproval } from '@/app/admin/utils'
import {
AdminErrorState,
AdminPageLayout,
AdminTableContainer,
AdminNotificationBanner,
AdminSearchFilters,
AdminStatsDisplay,
AdminTableNoResults,
ReviewRiskFilterButton,
} from '@/components/admin'
import { EmulatorIcon, SystemIcon } from '@/components/icons'
import {
ApproveButton,
AuthorRiskIndicator,
BulkActions,
Button,
ColumnVisibilityControl,
DisplayToggleButton,
LoadingSpinner,
LocalizedDate,
Pagination,
RejectButton,
ReviewRiskIndicator,
SortableHeader,
ViewButton,
Tooltip,
Expand Down Expand Up @@ -95,6 +96,11 @@ function PcListingApprovalsPage() {
)

const emulatorLogos = useEmulatorLogos()
const [selectedListingIds, setSelectedListingIds] = useState<string[]>([])
const reviewRiskFilter = useReviewRiskFilter({
clearSelection: () => setSelectedListingIds([]),
resetPage: () => table.setPage(1),
})

const currentUserQuery = api.users.me.useQuery()
const pendingPcListingsQuery = api.pcListings.pending.useQuery({
Expand All @@ -103,6 +109,7 @@ function PcListingApprovalsPage() {
sortField: table.sortField ?? undefined,
sortDirection: table.sortDirection ?? undefined,
search: isEmpty(table.search) ? undefined : table.search,
riskFilter: reviewRiskFilter.riskFilter,
})

const gameStatsQuery = api.games.stats.useQuery()
Expand All @@ -115,7 +122,6 @@ function PcListingApprovalsPage() {
useState<PendingPcListing | null>(null)
const [approvalNotes, setApprovalNotes] = useState('')
const [approvalDecision, setApprovalDecision] = useState<ApprovalStatus | null>(null)
const [selectedListingIds, setSelectedListingIds] = useState<string[]>([])
const confirm = useConfirmDialog()

const utils = api.useUtils()
Expand Down Expand Up @@ -261,16 +267,12 @@ function PcListingApprovalsPage() {

if (pendingPcListingsQuery.error) {
return (
<div className="container mx-auto px-4 py-8">
<div className="text-center py-12">
<p className="text-red-600 dark:text-red-400 text-lg">
Error loading pending PC listings: {pendingPcListingsQuery.error.message}
</p>
<Button onClick={() => pendingPcListingsQuery.refetch()} className="mt-4">
Try Again
</Button>
</div>
</div>
<AdminErrorState
message={`Error loading pending PC listings: ${pendingPcListingsQuery.error.message}`}
onRetry={() => {
void pendingPcListingsQuery.refetch()
}}
/>
)
}

Expand Down Expand Up @@ -343,7 +345,12 @@ function PcListingApprovalsPage() {
<AdminSearchFilters<PcApprovalSortField>
table={table}
searchPlaceholder="Search PC listings..."
/>
>
<ReviewRiskFilterButton
isActive={reviewRiskFilter.isRiskOnly}
onToggle={reviewRiskFilter.toggleRiskFilter}
/>
</AdminSearchFilters>

{pcListings.length > 0 && (
<BulkActions
Expand Down Expand Up @@ -375,7 +382,10 @@ function PcListingApprovalsPage() {
{pendingPcListingsQuery.isPending ? (
<LoadingSpinner text="Loading pending PC listings..." />
) : pcListings.length === 0 ? (
<AdminTableNoResults icon={Clock} hasQuery={!!table.search} />
<AdminTableNoResults
icon={Clock}
hasQuery={!!table.search || reviewRiskFilter.isRiskOnly}
/>
) : (
<div className="overflow-x-auto">
<table className="min-w-full">
Expand Down Expand Up @@ -560,8 +570,9 @@ function PcListingApprovalsPage() {
'N/A'
)}
</span>
<AuthorRiskIndicator
riskProfile={listing.authorRiskProfile}
<ReviewRiskIndicator
authorRiskProfile={listing.authorRiskProfile}
submissionRiskProfile={listing.submissionRiskProfile}
size="sm"
onInvestigate={(authorId) =>
router.push(`/admin/users?userId=${authorId}&tab=reports`)
Expand Down
Loading
Loading