'use client'; import React, { FC, useCallback, useMemo, useState } from 'react'; import useSWR from 'swr'; import copy from 'copy-to-clipboard'; import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; import { useUser } from '@gitroom/frontend/components/layout/user.context'; import { useToaster } from '@gitroom/react/toaster/toaster'; import { useModals } from '@gitroom/frontend/components/layout/new-modal'; import { Button } from '@gitroom/react/form/button'; import { LoadingComponent } from '@gitroom/frontend/components/layout/loading'; interface ErrorRow { id: string; message: string; body: string; platform: string; postId: string; createdAt: string; organization: { id: string; name: string; users: { user: { id: string; email: string; name: string | null } }[]; }; post: { id: string; content: string | null }; } interface ErrorsResponse { items: ErrorRow[]; total: number; page: number; limit: number; hasMore: boolean; } const safeParse = (value: string) => { try { return JSON.parse(value); } catch { return value; } }; const ErrorDetailsModal: FC<{ row: ErrorRow }> = ({ row }) => { const modal = useModals(); const toaster = useToaster(); const parsedMessage = useMemo(() => safeParse(row.message), [row.message]); const parsedBody = useMemo(() => safeParse(row.body), [row.body]); const copyAll = useCallback(() => { copy( JSON.stringify( { message: parsedMessage, body: parsedBody, meta: row }, null, 2 ) ); toaster.show('Debug code copied to clipboard', 'success'); }, [parsedMessage, parsedBody, row, toaster]); return (
Error Details
Platform
{row.platform}
Created
{new Date(row.createdAt).toLocaleString()}
Organization
{row.organization?.name}{' '} ({row.organization?.id})
Users
{row.organization?.users ?.map((u) => u.user?.email) .filter(Boolean) .join(', ') || '—'}
Post ID
{row.postId}
message
        {typeof parsedMessage === 'string'
          ? parsedMessage
          : JSON.stringify(parsedMessage, null, 2)}
      
body
        {typeof parsedBody === 'string'
          ? parsedBody
          : JSON.stringify(parsedBody, null, 2)}
      
); }; const usePlatformsList = () => { const fetch = useFetch(); return useSWR('/admin/errors/platforms', async (url: string) => { const res = await fetch(url); if (!res.ok) return []; return res.json(); }); }; const useErrorsList = (params: { page: number; limit: number; platform: string; email: string; unknownFirst: boolean; }) => { const fetch = useFetch(); const query = new URLSearchParams({ page: String(params.page), limit: String(params.limit), ...(params.platform ? { platform: params.platform } : {}), ...(params.email ? { email: params.email } : {}), unknownFirst: params.unknownFirst ? 'true' : 'false', }); const key = `/admin/errors?${query.toString()}`; return useSWR(key, async (url: string) => { const res = await fetch(url); if (!res.ok) { throw new Error('Failed to load errors'); } return res.json(); }); }; export const AdminErrorsComponent: FC = () => { const user = useUser(); const modal = useModals(); const toaster = useToaster(); const [page, setPage] = useState(0); const [limit, setLimit] = useState(20); const [platform, setPlatform] = useState(''); const [email, setEmail] = useState(''); const [emailInput, setEmailInput] = useState(''); const [unknownFirst, setUnknownFirst] = useState(true); const { data: platforms } = usePlatformsList(); const { data, isLoading, error } = useErrorsList({ page, limit, platform, email, unknownFirst, }); const onApplyEmail = useCallback(() => { setPage(0); setEmail(emailInput.trim()); }, [emailInput]); const onClear = useCallback(() => { setPage(0); setEmail(''); setEmailInput(''); setPlatform(''); }, []); const openDetails = useCallback( (row: ErrorRow) => { modal.openModal({ closeOnClickOutside: true, withCloseButton: false, classNames: { modal: 'w-[100%] max-w-[1100px] text-textColor', }, children: , }); }, [modal] ); const copyRow = useCallback( (row: ErrorRow) => { copy( JSON.stringify( { message: safeParse(row.message), body: safeParse(row.body), meta: row }, null, 2 ) ); toaster.show('Debug code copied to clipboard', 'success'); }, [toaster] ); if (!user?.isSuperAdmin) { return (
You do not have access to this page.
); } const totalPages = data ? Math.max(1, Math.ceil(data.total / limit)) : 1; return (
Errors
{data ? `${data.total} total` : ''}
Platform
Email contains
setEmailInput(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') onApplyEmail(); }} placeholder="user@example.com" className="bg-newBgColorInner h-[38px] border border-newTableBorder rounded-[8px] px-[10px] text-[14px] text-textColor min-w-[240px]" />
Per page
{isLoading ? ( ) : error ? (
Failed to load errors.
) : !data || data.items.length === 0 ? (
No errors found.
) : (
Created
Platform
User / Org
Message
Actions
{data.items.map((row) => { const isUnknown = (row.message || '').includes('Unknown Error'); const emails = row.organization?.users ?.map((u) => u.user?.email) .filter(Boolean) .join(', ') || '—'; const preview = (row.message || '').length > 280 ? row.message.slice(0, 280) + '…' : row.message; return (
{new Date(row.createdAt).toLocaleString()}
{row.platform}
{emails}
{row.organization?.name}
{preview}
); })}
)}
Page {page + 1} of {totalPages}
); };