feat: improve UI/UX consistency across all dashboard pages

Apply uniform design system across all routes and components:
- Consistent header pattern with gradient-text titles, dimmed subtitles
- Loader type="dots" replacing text-based loading states
- Icon + text empty/error states with Paper+glass containers
- Full STATUS_COLOR/STATUS_LABEL maps for all BugStatus values
- dayjs timestamps, Tooltip on action icons, size="sm" on badges/pagination
- Modals with overlayProps blur and gradient save buttons
- Replace left-border Papers with clean Stack headers
- Translate all remaining Indonesian UI strings to English
- New monitoring-themed SVG logo and redesigned splash screen
This commit is contained in:
2026-05-05 12:42:41 +08:00
parent ee543a16ad
commit ef852842b4
14 changed files with 1570 additions and 1365 deletions

View File

@@ -9,6 +9,7 @@ import {
Code,
Collapse,
Container,
FileInput,
Group,
Image,
Loader,
@@ -19,36 +20,54 @@ import {
SimpleGrid,
Stack,
Text,
ThemeIcon,
FileInput,
TextInput,
Textarea,
Title,
ThemeIcon,
Timeline,
Title,
Tooltip,
} from '@mantine/core'
import { useQuery } from '@tanstack/react-query'
import { createFileRoute } from '@tanstack/react-router'
import { useState } from 'react'
import { useDisclosure } from '@mantine/hooks'
import { notifications } from '@mantine/notifications'
import { useQuery } from '@tanstack/react-query'
import { createFileRoute } from '@tanstack/react-router'
import dayjs from 'dayjs'
import { useState } from 'react'
import {
TbAlertTriangle,
TbBug,
TbCircleCheck,
TbCircleX,
TbDeviceDesktop,
TbDeviceMobile,
TbFilter,
TbSearch,
TbHistory,
TbPhoto,
TbPlus,
TbCircleCheck,
TbCircleX,
TbSearch,
} from 'react-icons/tb'
export const Route = createFileRoute('/bug-reports')({
component: ListErrorsPage,
})
const STATUS_COLOR: Record<string, string> = {
OPEN: 'red',
IN_PROGRESS: 'blue',
ON_HOLD: 'orange',
RESOLVED: 'teal',
RELEASED: 'green',
CLOSED: 'gray',
}
const STATUS_LABEL: Record<string, string> = {
OPEN: 'Open',
ON_HOLD: 'On Hold',
IN_PROGRESS: 'In Progress',
RESOLVED: 'Resolved',
RELEASED: 'Released',
CLOSED: 'Closed',
}
function ListErrorsPage() {
const [page, setPage] = useState(1)
const [search, setSearch] = useState('')
@@ -58,29 +77,21 @@ function ListErrorsPage() {
const [showLogs, setShowLogs] = useState<Record<string, boolean>>({})
const [showStackTrace, setShowStackTrace] = useState<Record<string, boolean>>({})
const toggleLogs = (bugId: string) => {
setShowLogs((prev) => ({ ...prev, [bugId]: !prev[bugId] }))
}
const toggleStackTrace = (bugId: string) => {
setShowStackTrace((prev) => ({ ...prev, [bugId]: !prev[bugId] }))
}
const toggleLogs = (bugId: string) => setShowLogs((prev) => ({ ...prev, [bugId]: !prev[bugId] }))
const toggleStackTrace = (bugId: string) => setShowStackTrace((prev) => ({ ...prev, [bugId]: !prev[bugId] }))
const { data, isLoading, refetch } = useQuery({
queryKey: ['bugs', { page, search, app, status }],
queryFn: () =>
fetch(API_URLS.getBugs(page, search, app, status)).then((r) => r.json()),
queryFn: () => fetch(API_URLS.getBugs(page, search, app, status)).then((r) => r.json()),
})
// Fetch apps for the dropdown
const { data: appsList } = useQuery({
queryKey: ['apps-list'],
queryFn: () => fetch('/api/apps').then((r) => r.json()),
})
// Image Preview
const [previewImage, setPreviewImage] = useState<string | null>(null)
// Create Bug Modal Logic
const [opened, { open, close }] = useDisclosure(false)
const [isSubmitting, setIsSubmitting] = useState(false)
const [imageFiles, setImageFiles] = useState<File[]>([])
@@ -94,25 +105,17 @@ function ListErrorsPage() {
stackTrace: '',
})
// Update Status Modal Logic
const [updateModalOpened, { open: openUpdateModal, close: closeUpdateModal }] = useDisclosure(false)
const [isUpdating, setIsUpdating] = useState(false)
const [selectedBugId, setSelectedBugId] = useState<string | null>(null)
const [updateForm, setUpdateForm] = useState({
status: '',
description: '',
})
const [updateForm, setUpdateForm] = useState({ status: '', description: '' })
// Feedback Modal Logic
const [feedbackModalOpened, { open: openFeedbackModal, close: closeFeedbackModal }] = useDisclosure(false)
const [isUpdatingFeedback, setIsUpdatingFeedback] = useState(false)
const [feedbackForm, setFeedbackForm] = useState({
feedBack: '',
})
const [feedbackForm, setFeedbackForm] = useState({ feedBack: '' })
const handleUpdateFeedback = async () => {
if (!selectedBugId || !feedbackForm.feedBack) return
setIsUpdatingFeedback(true)
try {
const res = await fetch(API_URLS.updateBugFeedback(selectedBugId), {
@@ -120,27 +123,16 @@ function ListErrorsPage() {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(feedbackForm),
})
if (res.ok) {
notifications.show({
title: 'Success',
message: 'Feedback has been updated.',
color: 'teal',
icon: <TbCircleCheck size={18} />,
})
notifications.show({ title: 'Success', message: 'Feedback has been updated.', color: 'teal', icon: <TbCircleCheck size={18} /> })
refetch()
closeFeedbackModal()
setFeedbackForm({ feedBack: '' })
} else {
throw new Error('Failed to update feedback')
throw new Error()
}
} catch (e) {
notifications.show({
title: 'Error',
message: 'Something went wrong.',
color: 'red',
icon: <TbCircleX size={18} />,
})
} catch {
notifications.show({ title: 'Error', message: 'Something went wrong.', color: 'red', icon: <TbCircleX size={18} /> })
} finally {
setIsUpdatingFeedback(false)
}
@@ -148,7 +140,6 @@ function ListErrorsPage() {
const handleUpdateStatus = async () => {
if (!selectedBugId || !updateForm.status) return
setIsUpdating(true)
try {
const res = await fetch(API_URLS.updateBugStatus(selectedBugId), {
@@ -156,27 +147,16 @@ function ListErrorsPage() {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updateForm),
})
if (res.ok) {
notifications.show({
title: 'Success',
message: 'Status has been updated.',
color: 'teal',
icon: <TbCircleCheck size={18} />,
})
notifications.show({ title: 'Success', message: 'Status has been updated.', color: 'teal', icon: <TbCircleCheck size={18} /> })
refetch()
closeUpdateModal()
setUpdateForm({ status: '', description: '' })
} else {
throw new Error('Failed to update status')
throw new Error()
}
} catch (e) {
notifications.show({
title: 'Error',
message: 'Something went wrong.',
color: 'red',
icon: <TbCircleX size={18} />,
})
} catch {
notifications.show({ title: 'Error', message: 'Something went wrong.', color: 'red', icon: <TbCircleX size={18} /> })
} finally {
setIsUpdating(false)
}
@@ -184,14 +164,9 @@ function ListErrorsPage() {
const handleCreateBug = async () => {
if (!createForm.description || !createForm.affectedVersion || !createForm.device || !createForm.os) {
notifications.show({
title: 'Validation Error',
message: 'Please fill in all required fields.',
color: 'red',
})
notifications.show({ title: 'Validation Error', message: 'Please fill in all required fields.', color: 'red' })
return
}
setIsSubmitting(true)
try {
const imageUrls: string[] = []
@@ -199,52 +174,31 @@ function ListErrorsPage() {
const formData = new FormData()
formData.append('file', file)
const uploadRes = await fetch(API_URLS.uploadImage(), { method: 'POST', body: formData })
if (!uploadRes.ok) throw new Error('Gagal mengupload gambar')
if (!uploadRes.ok) throw new Error('Failed to upload image')
const { url } = await uploadRes.json()
imageUrls.push(url)
}
const res = await fetch(API_URLS.createBug(), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...createForm, imageUrls: imageUrls.length ? imageUrls : undefined }),
})
if (res.ok) {
await fetch(API_URLS.createLog(), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'CREATE', message: `Report error baru ditambahkan: ${createForm.description.substring(0, 50)}${createForm.description.length > 50 ? '...' : ''}` })
body: JSON.stringify({ type: 'CREATE', message: `New error report added: ${createForm.description.substring(0, 50)}${createForm.description.length > 50 ? '...' : ''}` }),
}).catch(console.error)
notifications.show({
title: 'Success',
message: 'Error report has been created.',
color: 'teal',
icon: <TbCircleCheck size={18} />,
})
notifications.show({ title: 'Success', message: 'Error report has been created.', color: 'teal', icon: <TbCircleCheck size={18} /> })
refetch()
close()
setImageFiles([])
setCreateForm({
description: '',
app: 'desa-plus',
source: 'USER',
affectedVersion: '',
device: '',
os: '',
stackTrace: '',
})
setCreateForm({ description: '', app: 'desa-plus', source: 'USER', affectedVersion: '', device: '', os: '', stackTrace: '' })
} else {
throw new Error('Failed to create error report')
throw new Error()
}
} catch (e) {
notifications.show({
title: 'Error',
message: 'Something went wrong.',
color: 'red',
icon: <TbCircleX size={18} />,
})
} catch {
notifications.show({ title: 'Error', message: 'Something went wrong.', color: 'red', icon: <TbCircleX size={18} /> })
} finally {
setIsSubmitting(false)
}
@@ -257,28 +211,22 @@ function ListErrorsPage() {
<DashboardLayout>
<Container size="xl" py="lg">
<Stack gap="xl">
<Group justify="space-between" align="center">
<Stack gap={0}>
<Title order={2} className="gradient-text">
Error Reports
</Title>
<Group justify="space-between" align="flex-start">
<Stack gap={4}>
<Title order={2} className="gradient-text">Error Reports</Title>
<Text size="sm" c="dimmed">
Centralized error tracking and analysis for all applications.
</Text>
</Stack>
<Group>
<Button
variant="gradient"
gradient={{ from: '#2563EB', to: '#7C3AED', deg: 135 }}
leftSection={<TbPlus size={18} />}
onClick={open}
>
Report Error
</Button>
{/* <Button variant="light" color="red" leftSection={<TbBug size={16} />}>
Generate Report
</Button> */}
</Group>
<Button
variant="gradient"
gradient={{ from: '#2563EB', to: '#7C3AED', deg: 135 }}
leftSection={<TbPlus size={18} />}
size="sm"
onClick={open}
>
Report Error
</Button>
</Group>
{/* Image Preview Modal */}
@@ -286,7 +234,7 @@ function ListErrorsPage() {
opened={!!previewImage}
onClose={() => setPreviewImage(null)}
size="xl"
radius="xl"
radius="md"
padding={0}
withCloseButton={false}
overlayProps={{ backgroundOpacity: 0.75, blur: 6 }}
@@ -294,12 +242,7 @@ function ListErrorsPage() {
onClick={() => setPreviewImage(null)}
>
{previewImage && (
<Image
src={previewImage}
alt="Preview"
fit="contain"
style={{ maxHeight: '85vh', width: '100%' }}
/>
<Image src={previewImage} alt="Preview" fit="contain" style={{ maxHeight: '85vh', width: '100%' }} />
)}
</Modal>
@@ -307,28 +250,21 @@ function ListErrorsPage() {
opened={updateModalOpened}
onClose={closeUpdateModal}
title={<Text fw={700} size="lg">Update Bug Status</Text>}
radius="xl"
radius="md"
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
>
<Stack gap="md">
<Select
label="New Status"
placeholder="Select status"
placeholder="Select a status"
required
data={[
{ value: 'OPEN', label: 'Open' },
{ value: 'ON_HOLD', label: 'On Hold' },
{ value: 'IN_PROGRESS', label: 'In Progress' },
{ value: 'RESOLVED', label: 'Resolved' },
{ value: 'RELEASED', label: 'Released' },
{ value: 'CLOSED', label: 'Closed' },
]}
data={Object.entries(STATUS_LABEL).map(([value, label]) => ({ value, label }))}
value={updateForm.status}
onChange={(val) => setUpdateForm({ ...updateForm, status: val || '' })}
/>
<Textarea
label="Update Note (Optional)"
placeholder="E.g. Fixed in commit xxxxx / Assigned to team"
placeholder="e.g. Fixed in commit abc123 / Assigned to team"
minRows={3}
value={updateForm.description}
onChange={(e) => setUpdateForm({ ...updateForm, description: e.target.value })}
@@ -350,7 +286,7 @@ function ListErrorsPage() {
opened={feedbackModalOpened}
onClose={closeFeedbackModal}
title={<Text fw={700} size="lg">Developer Feedback</Text>}
radius="xl"
radius="md"
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
>
<Stack gap="md">
@@ -361,7 +297,7 @@ function ListErrorsPage() {
required
minRows={4}
value={feedbackForm.feedBack}
onChange={(e) => setFeedbackForm({ ...feedbackForm, feedBack: e.target.value })}
onChange={(e) => setFeedbackForm({ feedBack: e.target.value })}
/>
<Button
fullWidth
@@ -378,9 +314,9 @@ function ListErrorsPage() {
<Modal
opened={opened}
onClose={() => { close(); setImageFiles([]); }}
onClose={() => { close(); setImageFiles([]) }}
title={<Text fw={700} size="lg">Report New Error</Text>}
radius="xl"
radius="md"
size="lg"
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
>
@@ -393,7 +329,6 @@ function ListErrorsPage() {
value={createForm.description}
onChange={(e) => setCreateForm({ ...createForm, description: e.target.value })}
/>
<SimpleGrid cols={2}>
<Select
label="Application"
@@ -414,19 +349,17 @@ function ListErrorsPage() {
onChange={(val) => setCreateForm({ ...createForm, source: val as any })}
/>
</SimpleGrid>
<TextInput
label="Version"
label="Affected Version"
placeholder="e.g. 2.4.1"
required
value={createForm.affectedVersion}
onChange={(e) => setCreateForm({ ...createForm, affectedVersion: e.target.value })}
/>
<SimpleGrid cols={2}>
<TextInput
label="Device"
placeholder="e.g. iPhone 13, Windows 11 PC"
placeholder="e.g. iPhone 13, Windows PC"
required
value={createForm.device}
onChange={(e) => setCreateForm({ ...createForm, device: e.target.value })}
@@ -439,17 +372,16 @@ function ListErrorsPage() {
onChange={(e) => setCreateForm({ ...createForm, os: e.target.value })}
/>
</SimpleGrid>
<FileInput
label="Screenshot (Optional)"
placeholder="Klik untuk upload gambar..."
label="Screenshots (Optional)"
placeholder="Click to upload images..."
accept="image/*"
leftSection={<TbPhoto size={16} />}
description="Maks 3 gambar · 5MB per file · JPG, PNG, WEBP"
description="Max 3 images · 5 MB each · JPG, PNG, WEBP"
value={imageFiles}
onChange={(files) => {
if (files.length > 3) {
notifications.show({ title: 'Error', message: 'Maksimal 3 gambar', color: 'red' })
notifications.show({ title: 'Error', message: 'Maximum 3 images allowed.', color: 'red' })
return
}
setImageFiles(files)
@@ -457,16 +389,14 @@ function ListErrorsPage() {
clearable
multiple
/>
<Textarea
label="Stack Trace (Optional)"
placeholder="Paste code or error logs here..."
placeholder="Paste error logs or stack trace here..."
style={{ fontFamily: 'monospace' }}
minRows={2}
value={createForm.stackTrace}
onChange={(e) => setCreateForm({ ...createForm, stackTrace: e.target.value })}
/>
<Button
fullWidth
mt="md"
@@ -481,16 +411,19 @@ function ListErrorsPage() {
</Modal>
<Paper withBorder radius="2xl" className="glass" p="md">
<SimpleGrid cols={{ base: 1, sm: 4 }} mb="md">
<SimpleGrid cols={{ base: 1, sm: 4 }} mb="lg">
<TextInput
placeholder="Search description, device, os..."
label="Search"
placeholder="Description, device, OS..."
leftSection={<TbSearch size={16} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
radius="md"
size="sm"
/>
<Select
placeholder="Application"
label="Application"
size="sm"
data={[
{ value: 'all', label: 'All Applications' },
...(appsList?.map((a: any) => ({ value: a.id, label: a.name })) || []),
@@ -501,38 +434,38 @@ function ListErrorsPage() {
disabled={!appsList}
/>
<Select
placeholder="Status"
label="Status"
size="sm"
data={[
{ value: 'all', label: 'All Status' },
{ value: 'OPEN', label: 'Open' },
{ value: 'ON_HOLD', label: 'On Hold' },
{ value: 'IN_PROGRESS', label: 'In Progress' },
{ value: 'RESOLVED', label: 'Resolved' },
{ value: 'RELEASED', label: 'Released' },
{ value: 'CLOSED', label: 'Closed' },
...Object.entries(STATUS_LABEL).map(([value, label]) => ({ value, label })),
]}
value={status}
onChange={(val) => setStatus(val || 'all')}
radius="md"
/>
<Group justify="flex-end">
<Button variant="subtle" color="gray" leftSection={<TbFilter size={16} />} onClick={() => {setSearch(''); setApp('all'); setStatus('all')}}>
Reset
<Stack justify="flex-end">
<Button
variant="filled"
color="violet"
size="sm"
onClick={() => { setSearch(''); setApp('all'); setStatus('all') }}
>
Reset Filters
</Button>
</Group>
</Stack>
</SimpleGrid>
{isLoading ? (
<Stack align="center" py="xl">
<Loader size="lg" type="dots" />
<Text size="sm" c="dimmed">Loading error reports...</Text>
<Loader size="md" type="dots" />
</Stack>
) : bugs.length === 0 ? (
<Paper p="xl" withBorder style={{ borderStyle: 'dashed', textAlign: 'center' }}>
<TbBug size={48} color="gray" style={{ marginBottom: 12, opacity: 0.5 }} />
<Text fw={600}>No error reports found</Text>
<Stack align="center" py="xl" gap="xs">
<TbBug size={40} style={{ opacity: 0.25 }} />
<Text fw={600} size="sm">No error reports found</Text>
<Text size="sm" c="dimmed">Try adjusting your filters or search terms.</Text>
</Paper>
</Stack>
) : (
<Accordion variant="separated" radius="xl">
{bugs.map((bug: any) => (
@@ -542,19 +475,13 @@ function ListErrorsPage() {
style={{
border: '1px solid var(--mantine-color-default-border)',
background: 'var(--mantine-color-default)',
marginBottom: '12px',
marginBottom: 12,
}}
>
<Accordion.Control>
<Group wrap="nowrap">
<ThemeIcon
color={
bug.status === 'OPEN'
? 'red'
: bug.status === 'IN_PROGRESS'
? 'blue'
: 'teal'
}
color={STATUS_COLOR[bug.status] ?? 'gray'}
variant="light"
size="lg"
radius="md"
@@ -563,37 +490,27 @@ function ListErrorsPage() {
</ThemeIcon>
<Box style={{ flex: 1 }}>
<Group justify="space-between">
<Text size="sm" fw={600} lineClamp={1}>
{bug.description}
</Text>
<Text size="sm" fw={600} lineClamp={1}>{bug.description}</Text>
<Badge
color={
bug.status === 'OPEN'
? 'red'
: bug.status === 'IN_PROGRESS'
? 'blue'
: 'teal'
}
color={STATUS_COLOR[bug.status] ?? 'gray'}
variant="dot"
size="xs"
size="sm"
>
{bug.status}
{STATUS_LABEL[bug.status] ?? bug.status}
</Badge>
</Group>
<Group gap="md">
<Text size="xs" c="dimmed">
{new Date(bug.createdAt).toLocaleString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false })} {bug.appId?.toUpperCase()} v{bug.affectedVersion}
</Text>
</Group>
<Text size="xs" c="dimmed">
{dayjs(bug.createdAt).format('D MMM YYYY, HH:mm')} · {bug.appId?.toUpperCase()} · v{bug.affectedVersion}
</Text>
</Box>
</Group>
</Accordion.Control>
<Accordion.Panel>
<Stack gap="lg" py="xs">
{/* Device Info */}
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="lg">
<Box>
<Text size="xs" fw={700} c="dimmed" mb={4}>DEVICE METADATA</Text>
<Text size="xs" fw={700} c="dimmed" mb={4} tt="uppercase">Device Metadata</Text>
<Group gap="xs">
{bug.device.toLowerCase().includes('windows') || bug.device.toLowerCase().includes('mac') || bug.device.toLowerCase().includes('pc') ? (
<TbDeviceDesktop size={14} color="gray" />
@@ -604,17 +521,16 @@ function ListErrorsPage() {
</Group>
</Box>
<Box>
<Text size="xs" fw={700} c="dimmed" mb={4}>SOURCE</Text>
<Text size="xs" fw={700} c="dimmed" mb={4} tt="uppercase">Source</Text>
<Badge variant="light" color="gray" size="sm">{bug.source}</Badge>
</Box>
</SimpleGrid>
{/* Feedback & Reporter Info */}
{(bug.user || bug.feedBack) && (
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="lg">
{bug.user && (
<Box>
<Text size="xs" fw={700} c="dimmed" mb={4}>REPORTED BY</Text>
<Text size="xs" fw={700} c="dimmed" mb={4} tt="uppercase">Reported By</Text>
<Group gap="xs">
<Avatar src={bug.user.image} radius="xl" size="sm" color="blue">
{bug.user.name?.charAt(0).toUpperCase()}
@@ -625,24 +541,18 @@ function ListErrorsPage() {
)}
{bug.feedBack && (
<Box>
<Text size="xs" fw={700} c="dimmed" mb={4}>DEVELOPER FEEDBACK</Text>
<Text size="xs" fw={700} c="dimmed" mb={4} tt="uppercase">Developer Feedback</Text>
<Text size="sm" style={{ whiteSpace: 'pre-wrap' }}>{bug.feedBack}</Text>
</Box>
)}
</SimpleGrid>
)}
{/* Stack Trace */}
{bug.stackTrace && (
<Box>
<Group justify="space-between" mb={showStackTrace[bug.id] ? 8 : 0}>
<Text size="xs" fw={700} c="dimmed">STACK TRACE</Text>
<Button
variant="subtle"
size="compact-xs"
color="gray"
onClick={() => toggleStackTrace(bug.id)}
>
<Text size="xs" fw={700} c="dimmed" tt="uppercase">Stack Trace</Text>
<Button variant="subtle" size="compact-xs" color="gray" onClick={() => toggleStackTrace(bug.id)}>
{showStackTrace[bug.id] ? 'Hide' : 'Show'}
</Button>
</Group>
@@ -650,12 +560,7 @@ function ListErrorsPage() {
<Code
block
color="red"
style={{
fontFamily: 'monospace',
whiteSpace: 'pre-wrap',
fontSize: '11px',
border: '1px solid var(--mantine-color-default-border)',
}}
style={{ fontFamily: 'monospace', whiteSpace: 'pre-wrap', fontSize: 11, border: '1px solid var(--mantine-color-default-border)' }}
>
{bug.stackTrace}
</Code>
@@ -663,43 +568,41 @@ function ListErrorsPage() {
</Box>
)}
{/* Images */}
{bug.images && bug.images.length > 0 && (
<Box>
<Group gap="xs" mb={8}>
<TbPhoto size={16} color="gray" />
<Text size="xs" fw={700} c="dimmed">ATTACHED IMAGES ({bug.images.length})</Text>
<TbPhoto size={14} color="gray" />
<Text size="xs" fw={700} c="dimmed" tt="uppercase">
Attached Images ({bug.images.length})
</Text>
</Group>
<SimpleGrid cols={{ base: 2, sm: 4 }} spacing="xs">
{bug.images.map((img: any) => (
<Paper
key={img.id}
withBorder
radius="md"
style={{ overflow: 'hidden', cursor: 'zoom-in' }}
onClick={() => setPreviewImage(img.imageUrl)}
>
<Image src={img.imageUrl} alt="Error screenshot" height={100} fit="cover" />
</Paper>
<Tooltip key={img.id} label="Click to preview" withArrow>
<Paper
withBorder
radius="md"
style={{ overflow: 'hidden', cursor: 'zoom-in' }}
onClick={() => setPreviewImage(img.imageUrl)}
>
<Image src={img.imageUrl} alt="Error screenshot" height={100} fit="cover" />
</Paper>
</Tooltip>
))}
</SimpleGrid>
</Box>
)}
{/* Logs / History */}
{bug.logs && bug.logs.length > 0 && (
<Box>
<Group justify="space-between" mb={showLogs[bug.id] ? 12 : 0}>
<Group gap="xs">
<TbHistory size={16} color="gray" />
<Text size="xs" fw={700} c="dimmed">ACTIVITY LOG ({bug.logs.length})</Text>
<TbHistory size={14} color="gray" />
<Text size="xs" fw={700} c="dimmed" tt="uppercase">
Activity Log ({bug.logs.length})
</Text>
</Group>
<Button
variant="subtle"
size="compact-xs"
color="gray"
onClick={() => toggleLogs(bug.id)}
>
<Button variant="subtle" size="compact-xs" color="gray" onClick={() => toggleLogs(bug.id)}>
{showLogs[bug.id] ? 'Hide' : 'Show'}
</Button>
</Group>
@@ -709,12 +612,16 @@ function ListErrorsPage() {
<Timeline.Item
key={log.id}
bullet={
<Badge size="xs" circle color={log.status === 'RESOLVED' ? 'teal' : 'blue'}> </Badge>
<Badge size="xs" circle color={STATUS_COLOR[log.status] ?? 'blue'}> </Badge>
}
title={
<Text size="sm" fw={600}>
{STATUS_LABEL[log.status] ?? log.status}
</Text>
}
title={<Text size="sm" fw={600}>{log.status}</Text>}
>
<Text size="xs" c="dimmed" mb={4}>
{new Date(log.createdAt).toLocaleString()} by {log.user?.name || 'Unknown'}
{dayjs(log.createdAt).format('D MMM YYYY, HH:mm')} · {log.user?.name ?? 'Unknown'}
</Text>
<Text size="sm">{log.description}</Text>
</Timeline.Item>
@@ -725,16 +632,30 @@ function ListErrorsPage() {
)}
<Group justify="flex-end" pt="sm">
<Button variant="light" size="compact-xs" color="blue" onClick={() => {
setSelectedBugId(bug.id)
setFeedbackForm({ feedBack: bug.feedBack || '' })
openFeedbackModal()
}}>Developer Feedback</Button>
<Button variant="light" size="compact-xs" color="teal" onClick={() => {
setSelectedBugId(bug.id)
setUpdateForm({ status: bug.status, description: '' })
openUpdateModal()
}}>Update Status</Button>
<Button
variant="light"
size="compact-sm"
color="blue"
onClick={() => {
setSelectedBugId(bug.id)
setFeedbackForm({ feedBack: bug.feedBack || '' })
openFeedbackModal()
}}
>
Developer Feedback
</Button>
<Button
variant="light"
size="compact-sm"
color="teal"
onClick={() => {
setSelectedBugId(bug.id)
setUpdateForm({ status: bug.status, description: '' })
openUpdateModal()
}}
>
Update Status
</Button>
</Group>
</Stack>
</Accordion.Panel>
@@ -745,7 +666,7 @@ function ListErrorsPage() {
{totalPages > 1 && (
<Group justify="center" mt="xl">
<Pagination total={totalPages} value={page} onChange={setPage} radius="xl" />
<Pagination total={totalPages} value={page} onChange={setPage} size="sm" radius="xl" />
</Group>
)}
</Paper>