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

@@ -21,12 +21,14 @@ import {
TextInput,
ThemeIcon,
Timeline,
Title
Title,
Tooltip,
} from '@mantine/core'
import { useDisclosure } from '@mantine/hooks'
import { notifications } from '@mantine/notifications'
import { useQuery } from '@tanstack/react-query'
import { createFileRoute, useParams } from '@tanstack/react-router'
import dayjs from 'dayjs'
import { useState } from 'react'
import {
TbAlertTriangle,
@@ -39,7 +41,7 @@ import {
TbHistory,
TbPhoto,
TbPlus,
TbSearch
TbSearch,
} from 'react-icons/tb'
import { API_URLS } from '../config/api'
@@ -47,43 +49,48 @@ export const Route = createFileRoute('/apps/$appId/errors')({
component: AppErrorsPage,
})
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 AppErrorsPage() {
const { appId } = useParams({ from: '/apps/$appId/errors' })
const [page, setPage] = useState(1)
const [search, setSearch] = useState('')
const [app, setApp] = useState(appId)
const [status, setStatus] = useState('all')
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()),
queryKey: ['bugs', { page, search, app: appId, status }],
queryFn: () => fetch(API_URLS.getBugs(page, search, appId, 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[]>([])
@@ -97,25 +104,17 @@ function AppErrorsPage() {
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), {
@@ -123,27 +122,16 @@ function AppErrorsPage() {
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)
}
@@ -151,7 +139,6 @@ function AppErrorsPage() {
const handleUpdateStatus = async () => {
if (!selectedBugId || !updateForm.status) return
setIsUpdating(true)
try {
const res = await fetch(API_URLS.updateBugStatus(selectedBugId), {
@@ -159,27 +146,16 @@ function AppErrorsPage() {
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)
}
@@ -187,14 +163,9 @@ function AppErrorsPage() {
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[] = []
@@ -202,52 +173,31 @@ function AppErrorsPage() {
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: appId,
source: 'USER',
affectedVersion: '',
device: '',
os: '',
stackTrace: '',
})
setCreateForm({ description: '', app: appId, 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,16 +207,19 @@ function AppErrorsPage() {
const totalPages = data?.totalPages || 1
return (
<Stack gap="xl">
<Group justify="space-between" align="center">
<Stack gap={0}>
<Title order={3}>Error Reporting Center</Title>
<Text size="sm" c="dimmed">Advanced analysis of health issues and crashes for <b>{appId}</b>.</Text>
<Stack gap="xl" py="md">
<Group justify="space-between" align="flex-start">
<Stack gap={4}>
<Title order={3}>Error Reports</Title>
<Text size="sm" c="dimmed">
Bug reports and crash tracking for this application.
</Text>
</Stack>
<Button
variant="gradient"
gradient={{ from: '#2563EB', to: '#7C3AED', deg: 135 }}
leftSection={<TbPlus size={18} />}
size="sm"
onClick={open}
>
Report Error
@@ -278,7 +231,7 @@ function AppErrorsPage() {
opened={!!previewImage}
onClose={() => setPreviewImage(null)}
size="xl"
radius="xl"
radius="md"
padding={0}
withCloseButton={false}
overlayProps={{ backgroundOpacity: 0.75, blur: 6 }}
@@ -286,12 +239,7 @@ function AppErrorsPage() {
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>
@@ -299,28 +247,21 @@ function AppErrorsPage() {
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 })}
@@ -342,7 +283,7 @@ function AppErrorsPage() {
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">
@@ -353,7 +294,7 @@ function AppErrorsPage() {
required
minRows={4}
value={feedbackForm.feedBack}
onChange={(e) => setFeedbackForm({ ...feedbackForm, feedBack: e.target.value })}
onChange={(e) => setFeedbackForm({ feedBack: e.target.value })}
/>
<Button
fullWidth
@@ -370,9 +311,9 @@ function AppErrorsPage() {
<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 }}
>
@@ -385,7 +326,6 @@ function AppErrorsPage() {
value={createForm.description}
onChange={(e) => setCreateForm({ ...createForm, description: e.target.value })}
/>
<SimpleGrid cols={2}>
<Select
label="Application"
@@ -406,19 +346,17 @@ function AppErrorsPage() {
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 })}
@@ -431,17 +369,16 @@ function AppErrorsPage() {
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)
@@ -449,16 +386,14 @@ function AppErrorsPage() {
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"
@@ -473,47 +408,50 @@ function AppErrorsPage() {
</Modal>
<Paper withBorder radius="2xl" className="glass" p="md">
<SimpleGrid cols={{ base: 1, sm: 3 }} mb="md">
<SimpleGrid cols={{ base: 1, sm: 3 }} 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="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(''); setStatus('all') }}>
Reset
<Stack justify="flex-end">
<Button
variant="subtle"
color="gray"
size="sm"
leftSection={<TbFilter size={16} />}
onClick={() => { setSearch(''); 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) => (
@@ -523,19 +461,13 @@ function AppErrorsPage() {
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"
@@ -544,37 +476,27 @@ function AppErrorsPage() {
</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" />
@@ -585,17 +507,16 @@ function AppErrorsPage() {
</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()}
@@ -606,24 +527,18 @@ function AppErrorsPage() {
)}
{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={4}>
<Text size="xs" fw={700} c="dimmed">STACK TRACE</Text>
<Button
variant="subtle"
size="compact-xs"
color="gray"
onClick={() => toggleStackTrace(bug.id)}
>
<Group justify="space-between" mb={showStackTrace[bug.id] ? 8 : 0}>
<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>
@@ -631,12 +546,7 @@ function AppErrorsPage() {
<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>
@@ -644,43 +554,41 @@ function AppErrorsPage() {
</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>
@@ -690,12 +598,16 @@ function AppErrorsPage() {
<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>
@@ -706,16 +618,30 @@ function AppErrorsPage() {
)}
<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>
@@ -726,7 +652,7 @@ function AppErrorsPage() {
{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>