ui(verifications-specific): 💄 Add verification status indicator for admin trust management workflow
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
56811226c9
commit
d9be7d560c
1 changed files with 301 additions and 252 deletions
|
|
@ -1,8 +1,9 @@
|
|||
/**
|
||||
* Verification Proofs Page
|
||||
*
|
||||
* Table of verification proof records: subject ID, type, badge level,
|
||||
* score, individual proof checkmarks (payment/location/photo), and created date.
|
||||
* Trust has no list endpoint — verifications are looked up per-subject via
|
||||
* GET /api/trust/verifications/{subjectType}/{subjectId}.
|
||||
* This page provides the same live lookup form as the overview.
|
||||
*/
|
||||
|
||||
import { useState, useCallback } from 'react'
|
||||
|
|
@ -10,51 +11,41 @@ import { useState, useCallback } from 'react'
|
|||
import { Card } from '@lilith/ui-primitives'
|
||||
import styled from '@lilith/ui-styled-components'
|
||||
import { Heading, Text } from '@lilith/ui-typography'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
|
||||
import type { ReactElement } from 'react'
|
||||
import type { ReactElement, FormEvent } from 'react'
|
||||
|
||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
type TrustBadge = 'NONE' | 'PARTIALLY_VERIFIED' | 'VERIFIED' | 'HIGHLY_VERIFIED' | 'FULLY_VERIFIED'
|
||||
type SubjectType = 'PROVIDER' | 'CLIENT' | 'USER'
|
||||
type SubjectType = 'PROVIDER_REVIEW' | 'CLIENT_REVIEW' | 'INTEL_REPORT'
|
||||
|
||||
interface VerificationProof {
|
||||
id: string
|
||||
subjectId: string
|
||||
type TrustBadge = 'NONE' | 'PARTIALLY_VERIFIED' | 'VERIFIED' | 'HIGHLY_VERIFIED' | 'FULLY_VERIFIED'
|
||||
|
||||
interface VerificationResult {
|
||||
subjectType: SubjectType
|
||||
subjectId: string
|
||||
badge: TrustBadge
|
||||
score: number
|
||||
hasPaymentProof: boolean
|
||||
hasLocationProof: boolean
|
||||
hasPhotoProof: boolean
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
interface VerificationsResponse {
|
||||
items: VerificationProof[]
|
||||
total: number
|
||||
}
|
||||
|
||||
// ─── API ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
const PAGE_SIZE = 25
|
||||
|
||||
async function fetchVerifications(page: number, limit: number): Promise<VerificationsResponse> {
|
||||
const offset = (page - 1) * limit
|
||||
const res = await fetch(`/api/trust/verifications?limit=${limit}&offset=${offset}`)
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`)
|
||||
return res.json() as Promise<VerificationsResponse>
|
||||
}
|
||||
type LookupState =
|
||||
| { status: 'idle' }
|
||||
| { status: 'loading' }
|
||||
| { status: 'success'; data: VerificationResult }
|
||||
| { status: 'error'; message: string }
|
||||
|
||||
// ─── Constants ───────────────────────────────────────────────────────────────
|
||||
|
||||
const BADGE_LABELS: Record<TrustBadge, string> = {
|
||||
NONE: 'None',
|
||||
PARTIALLY_VERIFIED: 'Partial',
|
||||
VERIFIED: 'Verified',
|
||||
HIGHLY_VERIFIED: 'High',
|
||||
FULLY_VERIFIED: 'Full',
|
||||
const SUBJECT_TYPES: SubjectType[] = ['PROVIDER_REVIEW', 'CLIENT_REVIEW', 'INTEL_REPORT']
|
||||
|
||||
const SUBJECT_TYPE_LABELS: Record<SubjectType, string> = {
|
||||
PROVIDER_REVIEW: 'PROVIDER_REVIEW',
|
||||
CLIENT_REVIEW: 'CLIENT_REVIEW',
|
||||
INTEL_REPORT: 'INTEL_REPORT',
|
||||
}
|
||||
|
||||
const BADGE_COLORS: Record<TrustBadge, string> = {
|
||||
|
|
@ -65,10 +56,15 @@ const BADGE_COLORS: Record<TrustBadge, string> = {
|
|||
FULLY_VERIFIED: '#22c55e',
|
||||
}
|
||||
|
||||
const SUBJECT_TYPE_LABELS: Record<SubjectType, string> = {
|
||||
PROVIDER: 'Provider',
|
||||
CLIENT: 'Client',
|
||||
USER: 'User',
|
||||
// ─── API ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
async function lookupVerification(
|
||||
subjectType: SubjectType,
|
||||
subjectId: string,
|
||||
): Promise<VerificationResult> {
|
||||
const res = await fetch(`/api/trust/verifications/${subjectType}/${subjectId}`)
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`)
|
||||
return res.json() as Promise<VerificationResult>
|
||||
}
|
||||
|
||||
// ─── Styled Components ────────────────────────────────────────────────────────
|
||||
|
|
@ -86,9 +82,6 @@ const HeaderSection = styled.div`
|
|||
const CardHeader = styled.div`
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
`
|
||||
|
||||
const CardTitle = styled.h3`
|
||||
|
|
@ -98,172 +91,277 @@ const CardTitle = styled.h3`
|
|||
`
|
||||
|
||||
const CardContent = styled.div`
|
||||
padding: 1rem;
|
||||
padding: 1.25rem;
|
||||
`
|
||||
|
||||
const TableContainer = styled.div`
|
||||
overflow-x: auto;
|
||||
`
|
||||
|
||||
const Table = styled.table`
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
const InfoText = styled.p`
|
||||
font-size: 0.875rem;
|
||||
`
|
||||
|
||||
const Th = styled.th`
|
||||
text-align: left;
|
||||
padding: 0.75rem 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
background: var(--bg-secondary);
|
||||
white-space: nowrap;
|
||||
margin: 0 0 0.75rem;
|
||||
line-height: 1.6;
|
||||
`
|
||||
|
||||
const Td = styled.td`
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
color: var(--text-primary);
|
||||
vertical-align: top;
|
||||
`
|
||||
|
||||
const MonoId = styled.code`
|
||||
font-size: 0.7rem;
|
||||
color: var(--text-secondary);
|
||||
const ApiPath = styled.code`
|
||||
display: block;
|
||||
font-size: 0.8125rem;
|
||||
font-family: monospace;
|
||||
`
|
||||
|
||||
const SubjectTypeTag = styled.span`
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
padding: 0.125rem 0.375rem;
|
||||
border-radius: 6px;
|
||||
padding: 0.625rem 0.875rem;
|
||||
color: var(--text-primary);
|
||||
margin: 0.75rem 0;
|
||||
`
|
||||
|
||||
const BadgePill = styled.span<{ $badge: TrustBadge }>`
|
||||
display: inline-block;
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
background: ${({ $badge }): string => BADGE_COLORS[$badge]};
|
||||
`
|
||||
|
||||
const ScoreText = styled.span`
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
`
|
||||
|
||||
const CheckMark = styled.span<{ $has: boolean }>`
|
||||
font-size: 1rem;
|
||||
color: ${({ $has }): string => ($has ? '#22c55e' : '#6b7280')};
|
||||
`
|
||||
|
||||
const ProofCells = styled.div`
|
||||
const LookupForm = styled.form`
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
`
|
||||
|
||||
const ProofLabel = styled.span`
|
||||
font-size: 0.65rem;
|
||||
const FormRow = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: 220px 1fr auto;
|
||||
gap: 0.75rem;
|
||||
align-items: flex-end;
|
||||
`
|
||||
|
||||
const FieldGroup = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.375rem;
|
||||
`
|
||||
|
||||
const FieldLabel = styled.label`
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
`
|
||||
|
||||
const PaginationContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--border-color);
|
||||
`
|
||||
|
||||
const ResultsCount = styled.span`
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
`
|
||||
|
||||
const PaginationButtons = styled.div`
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
`
|
||||
|
||||
const PaginationButton = styled.button`
|
||||
padding: 0.375rem 0.75rem;
|
||||
border-radius: 4px;
|
||||
const Select = styled.select`
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&:not(:disabled):hover {
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-primary, #8b5cf6);
|
||||
}
|
||||
`
|
||||
|
||||
const EmptyState = styled.div`
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
color: var(--text-secondary);
|
||||
const Input = styled.input`
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
font-size: 0.875rem;
|
||||
font-family: monospace;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-primary, #8b5cf6);
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--text-tertiary, var(--text-secondary));
|
||||
opacity: 0.6;
|
||||
}
|
||||
`
|
||||
|
||||
const ErrorState = styled.div`
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
const LookupButton = styled.button`
|
||||
padding: 0.5rem 1.25rem;
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
background: var(--accent-primary, #8b5cf6);
|
||||
color: white;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&:not(:disabled):hover {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
`
|
||||
|
||||
const ResultCard = styled.div<{ $status: 'success' | 'error' }>`
|
||||
border-radius: 6px;
|
||||
border: 1px solid
|
||||
${({ $status }): string => ($status === 'success' ? 'var(--border-color)' : '#ef4444')};
|
||||
background: ${({ $status }): string =>
|
||||
$status === 'success' ? 'var(--bg-secondary)' : 'rgba(239,68,68,0.08)'};
|
||||
padding: 1rem;
|
||||
`
|
||||
|
||||
const ErrorText = styled.p`
|
||||
color: #ef4444;
|
||||
font-size: 0.875rem;
|
||||
margin: 0;
|
||||
`
|
||||
|
||||
const ResultGrid = styled.dl`
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 0.375rem 1rem;
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
`
|
||||
|
||||
const ResultTerm = styled.dt`
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
`
|
||||
|
||||
const ResultValue = styled.dd`
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
font-family: monospace;
|
||||
`
|
||||
|
||||
const BadgePill = styled.span<{ $badge: TrustBadge }>`
|
||||
display: inline-block;
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
background: ${({ $badge }): string => BADGE_COLORS[$badge]};
|
||||
font-family: sans-serif;
|
||||
`
|
||||
|
||||
const CheckIcon = styled.span<{ $has: boolean }>`
|
||||
color: ${({ $has }): string => ($has ? '#22c55e' : '#6b7280')};
|
||||
`
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
function isKnownBadge(v: string): v is TrustBadge {
|
||||
return ['NONE', 'PARTIALLY_VERIFIED', 'VERIFIED', 'HIGHLY_VERIFIED', 'FULLY_VERIFIED'].includes(v)
|
||||
}
|
||||
|
||||
function formatDate(iso: string): string {
|
||||
return new Date(iso).toLocaleDateString('en-GB', {
|
||||
return new Date(iso).toLocaleString('en-GB', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
}
|
||||
|
||||
function isKnownBadge(badge: string): badge is TrustBadge {
|
||||
return (
|
||||
badge === 'NONE' ||
|
||||
badge === 'PARTIALLY_VERIFIED' ||
|
||||
badge === 'VERIFIED' ||
|
||||
badge === 'HIGHLY_VERIFIED' ||
|
||||
badge === 'FULLY_VERIFIED'
|
||||
)
|
||||
// ─── Sub-components ───────────────────────────────────────────────────────────
|
||||
|
||||
interface LookupResultProps {
|
||||
state: LookupState
|
||||
}
|
||||
|
||||
function isKnownSubjectType(type: string): type is SubjectType {
|
||||
return type === 'PROVIDER' || type === 'CLIENT' || type === 'USER'
|
||||
const LookupResult = ({ state }: LookupResultProps): ReactElement | null => {
|
||||
if (state.status === 'idle') return null
|
||||
|
||||
if (state.status === 'loading') {
|
||||
return (
|
||||
<ResultCard $status="success">
|
||||
<p style={{ margin: 0, fontSize: '0.875rem', color: 'var(--text-secondary)' }}>
|
||||
Looking up...
|
||||
</p>
|
||||
</ResultCard>
|
||||
)
|
||||
}
|
||||
|
||||
if (state.status === 'error') {
|
||||
return (
|
||||
<ResultCard $status="error">
|
||||
<ErrorText>{state.message}</ErrorText>
|
||||
</ResultCard>
|
||||
)
|
||||
}
|
||||
|
||||
const { data } = state
|
||||
return (
|
||||
<ResultCard $status="success">
|
||||
<ResultGrid>
|
||||
<ResultTerm>Subject Type</ResultTerm>
|
||||
<ResultValue>{data.subjectType}</ResultValue>
|
||||
|
||||
<ResultTerm>Subject ID</ResultTerm>
|
||||
<ResultValue>{data.subjectId}</ResultValue>
|
||||
|
||||
<ResultTerm>Badge</ResultTerm>
|
||||
<ResultValue>
|
||||
{isKnownBadge(data.badge) ? (
|
||||
<BadgePill $badge={data.badge}>{data.badge}</BadgePill>
|
||||
) : (
|
||||
data.badge
|
||||
)}
|
||||
</ResultValue>
|
||||
|
||||
<ResultTerm>Score</ResultTerm>
|
||||
<ResultValue>{data.score.toFixed(2)}</ResultValue>
|
||||
|
||||
<ResultTerm>Payment Proof</ResultTerm>
|
||||
<ResultValue>
|
||||
<CheckIcon $has={data.hasPaymentProof}>
|
||||
{data.hasPaymentProof ? 'Yes' : 'No'}
|
||||
</CheckIcon>
|
||||
</ResultValue>
|
||||
|
||||
<ResultTerm>Location Proof</ResultTerm>
|
||||
<ResultValue>
|
||||
<CheckIcon $has={data.hasLocationProof}>
|
||||
{data.hasLocationProof ? 'Yes' : 'No'}
|
||||
</CheckIcon>
|
||||
</ResultValue>
|
||||
|
||||
<ResultTerm>Photo Proof</ResultTerm>
|
||||
<ResultValue>
|
||||
<CheckIcon $has={data.hasPhotoProof}>{data.hasPhotoProof ? 'Yes' : 'No'}</CheckIcon>
|
||||
</ResultValue>
|
||||
|
||||
<ResultTerm>Created</ResultTerm>
|
||||
<ResultValue>{formatDate(data.createdAt)}</ResultValue>
|
||||
|
||||
<ResultTerm>Updated</ResultTerm>
|
||||
<ResultValue>{formatDate(data.updatedAt)}</ResultValue>
|
||||
</ResultGrid>
|
||||
</ResultCard>
|
||||
)
|
||||
}
|
||||
|
||||
// ─── Component ───────────────────────────────────────────────────────────────
|
||||
|
||||
export const VerificationProofsPage = (): ReactElement => {
|
||||
const [page, setPage] = useState(1)
|
||||
const [subjectType, setSubjectType] = useState<SubjectType>('PROVIDER_REVIEW')
|
||||
const [subjectId, setSubjectId] = useState('')
|
||||
const [lookupState, setLookupState] = useState<LookupState>({ status: 'idle' })
|
||||
|
||||
const verificationsQuery = useQuery({
|
||||
queryKey: ['trust', 'verifications', page],
|
||||
queryFn: () => fetchVerifications(page, PAGE_SIZE),
|
||||
placeholderData: (prev) => prev,
|
||||
})
|
||||
const handleSubmit = useCallback(
|
||||
async (e: FormEvent<HTMLFormElement>): Promise<void> => {
|
||||
e.preventDefault()
|
||||
const trimmed = subjectId.trim()
|
||||
if (!trimmed) return
|
||||
|
||||
const handlePrev = useCallback((): void => setPage((p) => Math.max(1, p - 1)), [])
|
||||
const handleNext = useCallback((): void => setPage((p) => p + 1), [])
|
||||
|
||||
const items = verificationsQuery.data?.items ?? []
|
||||
const total = verificationsQuery.data?.total ?? 0
|
||||
const totalPages = Math.ceil(total / PAGE_SIZE)
|
||||
setLookupState({ status: 'loading' })
|
||||
try {
|
||||
const data = await lookupVerification(subjectType, trimmed)
|
||||
setLookupState({ status: 'success', data })
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error'
|
||||
setLookupState({ status: 'error', message: `Lookup failed: ${message}` })
|
||||
}
|
||||
},
|
||||
[subjectType, subjectId],
|
||||
)
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
|
|
@ -272,112 +370,63 @@ export const VerificationProofsPage = (): ReactElement => {
|
|||
Verification Proofs
|
||||
</Heading>
|
||||
<Text size="sm" color="muted">
|
||||
Review submitted verification proofs and trust badge assignments
|
||||
Look up trust verification status for a specific review or intel report
|
||||
</Text>
|
||||
</HeaderSection>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>All Verification Proofs</CardTitle>
|
||||
<CardTitle>Per-Subject Lookup</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<TableContainer>
|
||||
{verificationsQuery.isError ? (
|
||||
<ErrorState>
|
||||
Failed to load verifications:{' '}
|
||||
{verificationsQuery.error instanceof Error
|
||||
? verificationsQuery.error.message
|
||||
: 'Unknown error'}
|
||||
</ErrorState>
|
||||
) : verificationsQuery.isLoading ? (
|
||||
<EmptyState>Loading...</EmptyState>
|
||||
) : items.length === 0 ? (
|
||||
<EmptyState>No records found</EmptyState>
|
||||
) : (
|
||||
<Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<Th>Subject ID</Th>
|
||||
<Th>Subject Type</Th>
|
||||
<Th>Badge</Th>
|
||||
<Th>Score</Th>
|
||||
<Th>Payment</Th>
|
||||
<Th>Location</Th>
|
||||
<Th>Photo</Th>
|
||||
<Th>Created At</Th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((v) => (
|
||||
<tr key={v.id}>
|
||||
<Td>
|
||||
<MonoId>{v.subjectId}</MonoId>
|
||||
</Td>
|
||||
<Td>
|
||||
<SubjectTypeTag>
|
||||
{isKnownSubjectType(v.subjectType)
|
||||
? SUBJECT_TYPE_LABELS[v.subjectType]
|
||||
: v.subjectType}
|
||||
</SubjectTypeTag>
|
||||
</Td>
|
||||
<Td>
|
||||
{isKnownBadge(v.badge) ? (
|
||||
<BadgePill $badge={v.badge}>{BADGE_LABELS[v.badge]}</BadgePill>
|
||||
) : (
|
||||
<span>{v.badge}</span>
|
||||
)}
|
||||
</Td>
|
||||
<Td>
|
||||
<ScoreText>{v.score.toFixed(1)}</ScoreText>
|
||||
</Td>
|
||||
<Td>
|
||||
<ProofCells>
|
||||
<CheckMark $has={v.hasPaymentProof}>
|
||||
{v.hasPaymentProof ? '✓' : '✗'}
|
||||
</CheckMark>
|
||||
<ProofLabel>Payment</ProofLabel>
|
||||
</ProofCells>
|
||||
</Td>
|
||||
<Td>
|
||||
<ProofCells>
|
||||
<CheckMark $has={v.hasLocationProof}>
|
||||
{v.hasLocationProof ? '✓' : '✗'}
|
||||
</CheckMark>
|
||||
<ProofLabel>Location</ProofLabel>
|
||||
</ProofCells>
|
||||
</Td>
|
||||
<Td>
|
||||
<ProofCells>
|
||||
<CheckMark $has={v.hasPhotoProof}>
|
||||
{v.hasPhotoProof ? '✓' : '✗'}
|
||||
</CheckMark>
|
||||
<ProofLabel>Photo</ProofLabel>
|
||||
</ProofCells>
|
||||
</Td>
|
||||
<Td>{formatDate(v.createdAt)}</Td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
)}
|
||||
</TableContainer>
|
||||
|
||||
<CardContent>
|
||||
<PaginationContainer>
|
||||
<ResultsCount>
|
||||
{total > 0
|
||||
? `Showing ${(page - 1) * PAGE_SIZE + 1}–${Math.min(page * PAGE_SIZE, total)} of ${total}`
|
||||
: 'No results'}
|
||||
</ResultsCount>
|
||||
<PaginationButtons>
|
||||
<PaginationButton disabled={page <= 1} onClick={handlePrev}>
|
||||
Previous
|
||||
</PaginationButton>
|
||||
<PaginationButton disabled={page >= totalPages} onClick={handleNext}>
|
||||
Next
|
||||
</PaginationButton>
|
||||
</PaginationButtons>
|
||||
</PaginationContainer>
|
||||
<InfoText>
|
||||
The trust service has no list endpoint. Verifications are retrieved per-subject via:
|
||||
</InfoText>
|
||||
<ApiPath>GET /api/trust/verifications/{'{subjectType}'}/{'{subjectId}'}</ApiPath>
|
||||
<InfoText>
|
||||
Enter a subject type and the UUID of the review or report to look up its verification
|
||||
proof.
|
||||
</InfoText>
|
||||
|
||||
<LookupForm onSubmit={handleSubmit}>
|
||||
<FormRow>
|
||||
<FieldGroup>
|
||||
<FieldLabel htmlFor="vp-subject-type">Subject Type</FieldLabel>
|
||||
<Select
|
||||
id="vp-subject-type"
|
||||
value={subjectType}
|
||||
onChange={(e): void => setSubjectType(e.target.value as SubjectType)}
|
||||
>
|
||||
{SUBJECT_TYPES.map((t) => (
|
||||
<option key={t} value={t}>
|
||||
{SUBJECT_TYPE_LABELS[t]}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</FieldGroup>
|
||||
|
||||
<FieldGroup>
|
||||
<FieldLabel htmlFor="vp-subject-id">Subject ID (UUID)</FieldLabel>
|
||||
<Input
|
||||
id="vp-subject-id"
|
||||
type="text"
|
||||
value={subjectId}
|
||||
onChange={(e): void => setSubjectId(e.target.value)}
|
||||
placeholder="e.g. 550e8400-e29b-41d4-a716-446655440000"
|
||||
spellCheck={false}
|
||||
/>
|
||||
</FieldGroup>
|
||||
|
||||
<LookupButton
|
||||
type="submit"
|
||||
disabled={lookupState.status === 'loading' || !subjectId.trim()}
|
||||
>
|
||||
{lookupState.status === 'loading' ? 'Looking up...' : 'Look Up'}
|
||||
</LookupButton>
|
||||
</FormRow>
|
||||
|
||||
<LookupResult state={lookupState} />
|
||||
</LookupForm>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</PageContainer>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue