'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
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(', ') || '—'}
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` : ''}
{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}
);
};