diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 62842ba..c0d5b5a 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -9,6 +9,7 @@ import { useAuth } from './hooks/useAuth.ts' import { AuthBar } from './components/ui/AuthBar.tsx' import { BrowserRouter, Route, Routes } from 'react-router-dom' import { NotFound } from './pages/NotFound.tsx' +import { InfoBar } from './components/ui/InfoBar.tsx' type TabValue = 'tickets' | 'admin' | 'stats' @@ -50,7 +51,10 @@ function SupportApp() { return ( setShowLogin(true)} onLogout={logout} /> + <> + setShowLogin(true)} onLogout={logout} /> + + } > diff --git a/frontend/src/components/admin/AdminTable.tsx b/frontend/src/components/admin/AdminTable.tsx index 8f58025..bad42a2 100644 --- a/frontend/src/components/admin/AdminTable.tsx +++ b/frontend/src/components/admin/AdminTable.tsx @@ -115,98 +115,100 @@ export function AdminTable({ const hasBilling = tickets.some(t => t.type === 'billing') return ( -
- - - - {/* Select-all checkbox */} - - {(['Subject', 'User', 'Type', 'Status', ...(hasBilling ? ['Transaction'] : []), 'Description', 'Created'] as const).map(col => ( - + + + + {hasBilling && ( + + )} + + + + ) + })} + +
- - - {col} +
+
+ + + + {/* Select-all checkbox */} + - ))} - - - - {tickets.map(ticket => { - const { txnId, txnLine, body: displayDescription } = parseDescription(ticket.description) - const hasTxn = ticket.type === 'billing' && txnId !== null - const owned = isOwned(ticket) - const isSelected = selection.has(ticket.id) - - return ( - onOpen(ticket)} - > - {/* Row checkbox — stop propagation so clicking it doesn't open the modal */} - - - - - {hasBilling && ( - + + + {tickets.map(ticket => { + const { txnId, txnLine, body: displayDescription } = parseDescription(ticket.description) + const hasTxn = ticket.type === 'billing' && txnId !== null + const owned = isOwned(ticket) + const isSelected = selection.has(ticket.id) + + return ( + onOpen(ticket)} + > + {/* Row checkbox — stop propagation so clicking it doesn't open the modal */} + - )} - - - - ) - })} - -
+
e.stopPropagation()} + {(['Subject', 'User', 'Type', 'Status', ...(hasBilling ? ['Transaction'] : []), 'Description', 'Created'] as const).map(col => ( + - handleRowChange(ticket.id, checked)} - ariaLabel={`Select ticket: ${ticket.subject}`} - /> - - -
- {ticket.subject} - {owned && ( - - mine - - )} -
-
- {ticket.username ?? guest} - - {ticket.type.replace('-', ' ')} - - - - {hasTxn ? ( - - {txnLine!.split(' — ')[0]} - - ) : ( - - )} + {col} + + ))} +
e.stopPropagation()} + > + handleRowChange(ticket.id, checked)} + ariaLabel={`Select ticket: ${ticket.subject}`} + /> - - {displayDescription || No description} - - - {formatDate(ticket.createdAt)} -
+
+
+ {ticket.subject} + {owned && ( + + mine + + )} +
+
+ {ticket.username ?? guest} + + {ticket.type.replace('-', ' ')} + + + + {hasTxn ? ( + + {txnLine!.split(' — ')[0]} + + ) : ( + + )} + + + {displayDescription || No description} + + + {formatDate(ticket.createdAt)} +
+
{/* Pagination footer */}
diff --git a/frontend/src/components/icons/chevronIcon.tsx b/frontend/src/components/icons/chevronIcon.tsx new file mode 100644 index 0000000..150a83b --- /dev/null +++ b/frontend/src/components/icons/chevronIcon.tsx @@ -0,0 +1,7 @@ +import type { IconProps } from "../../lib/types.ts"; + +export const ChevronIcon = ({ className }: IconProps) => ( + +); diff --git a/frontend/src/components/tickets/TicketTable.tsx b/frontend/src/components/tickets/TicketTable.tsx index 30dfa7e..75cf2eb 100644 --- a/frontend/src/components/tickets/TicketTable.tsx +++ b/frontend/src/components/tickets/TicketTable.tsx @@ -23,7 +23,7 @@ export function TicketTable({ tickets, onOpen }: TicketTableProps) { } return ( -
+
diff --git a/frontend/src/components/ui/Badge.tsx b/frontend/src/components/ui/Badge.tsx index 42636d4..ecd35c1 100644 --- a/frontend/src/components/ui/Badge.tsx +++ b/frontend/src/components/ui/Badge.tsx @@ -14,7 +14,7 @@ interface BadgeProps { export function Badge({ status }: BadgeProps) { return ( {status} diff --git a/frontend/src/components/ui/InfoBar.tsx b/frontend/src/components/ui/InfoBar.tsx new file mode 100644 index 0000000..5ac99c3 --- /dev/null +++ b/frontend/src/components/ui/InfoBar.tsx @@ -0,0 +1,96 @@ +import { useState } from "react" +import type { User } from "../../lib/types" +import { InfoIcon } from "../icons/info" +import { CloseIcon } from "../icons/close" +const STORAGE_KEY = "info_bar_dismissed" +interface InfoBarProps { + user: User | null +} +export function InfoBar({ user }: InfoBarProps) { + const [open, setOpen] = useState( + () => localStorage.getItem(STORAGE_KEY) !== "true" + ) + function toggle() { + const next = !open + setOpen(next) + if (!next) { + localStorage.setItem(STORAGE_KEY, "true") + } else { + localStorage.removeItem(STORAGE_KEY) + } + } + const isGuest = user === null + return ( +
+ {/* Header row — always visible */} +
+
+ + + {isGuest ? "You're in guest mode" : "How this app works"} + +
+ +
+ {/* Collapsible body */} +
+
+
+ {isGuest ? ( +
+
+

Guest mode

+

+ Tickets are stored in your browser (localStorage) only. + No sync across devices or sessions. + Admin panel is not available. +

+
+
+

After signing in

+

+ Tickets are saved to the server persistently. + Admin panel: view all users' tickets. + You can only edit or manage your own tickets. +

+
+
+ ) : ( +
+
+

Admin Tab

+

+ You can view all users' tickets in the Admin tab. Useful for monitoring support requests +

+
+
+

Editing

+

+ You can only edit or manage your own tickets. Other users' tickets are read-only for you +

+
+
+ )} +
+
+
+
+ ) +} diff --git a/frontend/src/pages/AdminPage.tsx b/frontend/src/pages/AdminPage.tsx index e3a4641..14b90de 100644 --- a/frontend/src/pages/AdminPage.tsx +++ b/frontend/src/pages/AdminPage.tsx @@ -7,11 +7,12 @@ import type { PaginatedResponse, TicketFilters } from '../lib/storage.ts' import { useModal } from '../hooks/useModal.ts' import type { Ticket, User } from '../lib/types.ts' import { Button } from '../components/ui/Button.tsx' +import { ChevronIcon } from '../components/icons/chevronIcon.tsx' function StatCard({ label, value }: { label: string; value: number }) { return ( -
-

{label}

+
+

{label}

{value}

) @@ -61,7 +62,7 @@ function FilterBar({ filters, isAuthenticated, onChange }: FilterBarProps) { > {STATUS_OPTIONS.map(o => )} - +
{/* Type */} @@ -73,7 +74,7 @@ function FilterBar({ filters, isAuthenticated, onChange }: FilterBarProps) { > {TYPE_OPTIONS.map(o => )} - + {/* Mine toggle — only visible when authenticated */} @@ -112,17 +113,6 @@ function FilterBar({ filters, isAuthenticated, onChange }: FilterBarProps) { ) } -function ChevronIcon() { - return ( - - ) -} - interface AdminPageProps { isAuthenticated: boolean