Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions apps/web/src/app/admin/account-swaps/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { notFound } from "next/navigation"
import { AccountStatusBadge, StatusBadge, VerificationBadge } from "@/components/badges"
import { ActionForm, SubmitButton } from "@/components/forms"
import { Badge, type BadgeTone } from "@/components/ui/Badge"
import { Callout } from "@/components/ui/Callout"
import { EmptyState } from "@/components/ui/EmptyState"
import { Field } from "@/components/ui/Field"
import { Notice } from "@/components/ui/Notice"
import {
approvalBlock,
listDecidedSwaps,
Expand Down Expand Up @@ -187,11 +187,11 @@ function SwapCard({

{swap.status === "pending" &&
(own ? (
<Notice tone="warning">Đây là chiến dịch của bạn; một nhân viên khác cần xử lý.</Notice>
<Callout tone="warning">Đây là chiến dịch của bạn; một nhân viên khác cần xử lý.</Callout>
) : (
<ItemActions>
{block ? (
<Notice tone="warning">Chưa duyệt được: {block}</Notice>
<Callout tone="warning">Chưa duyệt được: {block}</Callout>
) : (
<ActionForm
action={decideAccountSwap.bind(null, swap.id)}
Expand Down
6 changes: 3 additions & 3 deletions apps/web/src/app/admin/accounts/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { notFound } from "next/navigation"
import { AccountStatusBadge, VerificationBadge } from "@/components/badges"
import { ActionForm, SubmitButton } from "@/components/forms"
import { Badge } from "@/components/ui/Badge"
import { Callout } from "@/components/ui/Callout"
import { EmptyState } from "@/components/ui/EmptyState"
import { Field } from "@/components/ui/Field"
import { Notice } from "@/components/ui/Notice"
import { requireSession } from "@/lib/auth/session"
import { bankByBin } from "@/lib/banks"
import { isStaff } from "@/lib/campaigns"
Expand Down Expand Up @@ -179,11 +179,11 @@ function AccountCard({
</Facts>

{own ? (
<Notice tone="warning">Đây là tài khoản của bạn; một nhân viên khác cần xử lý.</Notice>
<Callout tone="warning">Đây là tài khoản của bạn; một nhân viên khác cần xử lý.</Callout>
) : (
<ItemActions>
{status === "pending" && options.verifyBlocked && (
<Notice tone="warning">Chưa xác minh được: {options.verifyBlocked}</Notice>
<Callout tone="warning">Chưa xác minh được: {options.verifyBlocked}</Callout>
)}
{status === "pending" && !options.verifyBlocked && (
<ActionForm
Expand Down
6 changes: 3 additions & 3 deletions apps/web/src/app/admin/error.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
"use client"

import { Button } from "@/components/ui/Button"
import { Notice } from "@/components/ui/Notice"
import { Callout } from "@/components/ui/Callout"

/** An admin page that failed to load; the admin tabs stay so staff can go elsewhere. */
export default function AdminError({ retry }: { error: Error & { digest?: string }; retry: () => void }) {
return (
<div className="container-page py-8 sm:py-10">
<div className="max-w-2xl space-y-4">
<h1 className="text-2xl font-bold tracking-tight sm:text-3xl">Không tải được trang này</h1>
<Notice tone="danger">
<Callout tone="danger">
Có lỗi khi đọc dữ liệu của trang. Hãy thử lại; nếu vẫn lỗi, hãy tải lại trang sau ít phút.
</Notice>
</Callout>
<Button onClick={() => retry()}>Thử lại</Button>
</div>
</div>
Expand Down
6 changes: 3 additions & 3 deletions apps/web/src/app/admin/evidence/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import Link from "next/link"
import { notFound } from "next/navigation"

import { ActionForm, SubmitButton } from "@/components/forms"
import { Callout } from "@/components/ui/Callout"
import { EmptyState } from "@/components/ui/EmptyState"
import { Field } from "@/components/ui/Field"
import { Notice } from "@/components/ui/Notice"
import { requireSession } from "@/lib/auth/session"
import { isStaff } from "@/lib/campaigns"
import { HIDDEN_LIMIT, listModerationQueue, MODERATION_ROLES, type ModerationItem } from "@/lib/evidence-moderation"
Expand Down Expand Up @@ -124,10 +124,10 @@ function EvidenceCard({
)}
</p>
{isHidden && (
<Notice tone="danger">
<Callout tone="danger">
Đang ẩn từ {formatDateTime(item.state!.moderated_at)}
{item.state?.reason ? `: ${item.state.reason}` : ""}
</Notice>
</Callout>
)}
{item.reports.length > 0 && (
<div>
Expand Down
6 changes: 3 additions & 3 deletions apps/web/src/app/admin/inbound/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import type { Metadata } from "next"
import { notFound } from "next/navigation"

import { ActionForm, SubmitButton } from "@/components/forms"
import { Callout } from "@/components/ui/Callout"
import { EmptyState } from "@/components/ui/EmptyState"
import { Notice } from "@/components/ui/Notice"
import { requireSession } from "@/lib/auth/session"
import { isStaff } from "@/lib/campaigns"
import { formatDateTime } from "@/lib/format"
Expand Down Expand Up @@ -109,13 +109,13 @@ function ParkedCard({
{account ? ` · ${account}` : ""}
{part ? ` · phần ${part.number}/${part.count} của file` : ""} · nhận lúc {formatDateTime(event.received_at)}
</p>
<Notice tone="warning" title={parkedReasonLabel(event.last_error)}>
<Callout tone="warning" title={parkedReasonLabel(event.last_error)}>
<p className="text-sm">
Mã lỗi: <span className="font-mono">{event.last_error ?? "—"}</span> · đã thử {event.attempts} lần
{event.last_attempt_at ? `, lần cuối ${formatDateTime(event.last_attempt_at)}` : ""} · dừng lúc{" "}
{formatDateTime(event.parked_at)}
</p>
</Notice>
</Callout>
<p className="font-mono text-xs break-all text-muted">{event.id}</p>
</div>

Expand Down
12 changes: 6 additions & 6 deletions apps/web/src/app/admin/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import type { ReactNode } from "react"
import { TypeBadge, VerificationBadge } from "@/components/badges"
import { ActionForm, SubmitButton } from "@/components/forms"
import { ButtonLink } from "@/components/ui/Button"
import { Callout } from "@/components/ui/Callout"
import { EmptyState } from "@/components/ui/EmptyState"
import { Notice } from "@/components/ui/Notice"
import { requireSession } from "@/lib/auth/session"
import {
accountProblemLabel,
Expand Down Expand Up @@ -100,7 +100,7 @@ export default async function AdminReviewPage() {
)}
</Facts>
{own ? (
<Notice tone="warning">Đây là chiến dịch của bạn; một kiểm duyệt viên khác cần duyệt.</Notice>
<Callout tone="warning">Đây là chiến dịch của bạn; một kiểm duyệt viên khác cần duyệt.</Callout>
) : (
<ItemActions>
<ActionForm
Expand Down Expand Up @@ -146,9 +146,9 @@ export default async function AdminReviewPage() {
lead="Chiến dịch (từ thiện hoặc dự án có quà tặng) đã duyệt nhưng tài khoản nhận tiền không còn dùng được, nên hệ thống không tự mở. Chiến dịch tự mở nếu tài khoản dùng được trở lại; nếu không, hãy yêu cầu chỉnh sửa để người tổ chức nhả tài khoản cũ, gắn tài khoản khác rồi gửi duyệt lại."
>
{waiting === null ? (
<Notice tone="danger" title="Chưa tải được danh sách này.">
<Callout tone="danger" title="Chưa tải được danh sách này.">
Các phần khác của trang vẫn dùng được. Hãy tải lại trang sau.
</Notice>
</Callout>
) : waiting.length === 0 ? (
<EmptyState title="Không có chiến dịch đã duyệt nào đang chờ tài khoản nhận." />
) : (
Expand Down Expand Up @@ -180,9 +180,9 @@ export default async function AdminReviewPage() {
</Fact>
)}
</Facts>
{advice.note && <Notice tone="warning">{advice.note}</Notice>}
{advice.note && <Callout tone="warning">{advice.note}</Callout>}
{advice.sendBackReason === null ? null : own ? (
<Notice tone="warning">Đây là chiến dịch của bạn; một kiểm duyệt viên khác cần xử lý.</Notice>
<Callout tone="warning">Đây là chiến dịch của bạn; một kiểm duyệt viên khác cần xử lý.</Callout>
) : (
<ActionForm
action={requestChanges.bind(null, campaign.id)}
Expand Down
6 changes: 3 additions & 3 deletions apps/web/src/app/admin/payment-requests/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { notFound } from "next/navigation"
import { ActionForm, SubmitButton } from "@/components/forms"
import { Badge } from "@/components/ui/Badge"
import { ButtonLink } from "@/components/ui/Button"
import { Callout } from "@/components/ui/Callout"
import { EmptyState } from "@/components/ui/EmptyState"
import { Field } from "@/components/ui/Field"
import { Notice } from "@/components/ui/Notice"
import { requireSession } from "@/lib/auth/session"
import { isStaff } from "@/lib/campaigns"
import { formatDate, formatDateTime, formatVnd } from "@/lib/format"
Expand Down Expand Up @@ -100,14 +100,14 @@ function CampaignCard({ campaign }: { campaign: SucceededBackCampaign }) {

{stopped ? (
<>
<Notice
<Callout
tone="danger"
title={`Đã tạm dừng yêu cầu thanh toán từ ${formatDateTime(campaign.payments_stopped_at)}.`}
>
{campaign.payments_stopped_note && (
<p className="whitespace-pre-line">Ghi chú công khai: {campaign.payments_stopped_note}</p>
)}
</Notice>
</Callout>
<ItemActions>
<ActionForm
action={setPaymentRequestsStopped.bind(null, campaign.id)}
Expand Down
8 changes: 4 additions & 4 deletions apps/web/src/app/admin/statements/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { notFound } from "next/navigation"

import { ActionForm, SubmitButton } from "@/components/forms"
import { ButtonLink } from "@/components/ui/Button"
import { Callout } from "@/components/ui/Callout"
import { EmptyState } from "@/components/ui/EmptyState"
import { Notice } from "@/components/ui/Notice"
import { requireSession } from "@/lib/auth/session"
import { isStaff } from "@/lib/campaigns"
import { formatDate, formatDateTime, formatVnd } from "@/lib/format"
Expand Down Expand Up @@ -189,7 +189,7 @@ function QueueCard({
<p className="text-ink">
<RowFacts row={row} />
</p>
<Notice tone="warning">{ambiguousReasonLabel(row.ambiguous_reason)}</Notice>
<Callout tone="warning">{ambiguousReasonLabel(row.ambiguous_reason)}</Callout>
{candidate && (
<p className="rounded-md bg-surface p-3 text-sm">
Khớp với dòng {candidate.row_no} của một lần tải trước ({rowStatusLabel(candidate.status).toLowerCase()}):{" "}
Expand Down Expand Up @@ -238,10 +238,10 @@ function QueueCard({
))}
</fieldset>
{targets.total > targets.shown.length && (
<Notice tone="warning">
<Callout tone="warning">
Còn {targets.total - targets.shown.length} giao dịch khác cùng số tiền xa ngày hơn không hiện ở đây.
Nếu không thấy đúng giao dịch, đừng chọn đại một giao dịch.
</Notice>
</Callout>
)}
<NoteField id={`dup-note-${row.id}`} />
<SubmitButton className="btn-secondary">Là giao dịch đã ghi</SubmitButton>
Expand Down
25 changes: 25 additions & 0 deletions apps/web/src/app/me/_components/MeError.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"use client"

import { Button, ButtonLink } from "@/components/ui/Button"
import { Callout } from "@/components/ui/Callout"

/**
* What a /me section shows when its page fails to render: what happened,
* a retry, and a way back to the person's campaigns.
*/
export function MeError({ retry }: { retry: () => void }) {
return (
<div className="mx-auto w-full max-w-2xl px-4 py-16 sm:px-6">
<h1 className="mb-4 text-2xl font-bold tracking-tight">Không tải được trang này</h1>
<Callout tone="danger" role="alert">
Có thể kết nối bị gián đoạn hoặc máy chủ đang bận. Dữ liệu của bạn không bị ảnh hưởng; hãy thử lại sau ít phút.
</Callout>
<div className="mt-6 flex flex-wrap gap-3">
<Button onClick={() => retry()}>Thử lại</Button>
<ButtonLink href="/me/campaigns" variant="secondary">
Về Chiến dịch của tôi
</ButtonLink>
</div>
</div>
)
}
37 changes: 37 additions & 0 deletions apps/web/src/app/me/_components/MePage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { ReactNode } from "react"

import { TabNav } from "@/components/ui/Tabs"

export type MeSection = "campaigns" | "contributions" | "notices" | "accounts"

const SECTIONS: { key: MeSection; href: string; label: string }[] = [
{ key: "campaigns", href: "/me/campaigns", label: "Chiến dịch của tôi" },
{ key: "contributions", href: "/me/contributions", label: "Ủng hộ của tôi" },
{ key: "notices", href: "/me/notices", label: "Thông báo" },
{ key: "accounts", href: "/me/accounts", label: "Tài khoản nhận tiền" },
]

/**
* A top-level /me page (DOS/Give#96): the tabs between the signed-in
* person's lists, then the page. On phones the tabs scroll sideways.
*/
export function MePage({ section, children }: { section: MeSection; children: ReactNode }) {
return (
<div className="container-page pt-4 pb-16 sm:pt-6">
<TabNav
label="Trang của tôi"
items={SECTIONS.map((s) => ({ href: s.href, label: s.label, current: s.key === section }))}
/>
<div className="mt-8 space-y-8">{children}</div>
</div>
)
}

/** An inner /me page (an editor, a report): one centred column, no tabs. */
export function MeDetail({ wide, children }: { wide?: boolean; children: ReactNode }) {
return (
<div className={`mx-auto w-full px-4 pt-4 pb-16 sm:px-6 sm:pt-6 ${wide ? "max-w-6xl" : "max-w-4xl"}`}>
<div className="space-y-8">{children}</div>
</div>
)
}
45 changes: 45 additions & 0 deletions apps/web/src/app/me/_components/Panel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import clsx from "clsx"
import type { ReactNode } from "react"

/**
* One section of an inner /me page: a card with an h2, an optional lead and
* an optional action on the right. `id` is the anchor the page links to.
*/
export function Panel({
id,
title,
description,
action,
className,
children,
}: {
id: string
title: string
description?: ReactNode
action?: ReactNode
className?: string
children?: ReactNode
}) {
return (
<section id={id} aria-labelledby={`${id}-title`} className={clsx("scroll-mt-24 card p-5 sm:p-6", className)}>
<div className="flex flex-wrap items-start justify-between gap-x-6 gap-y-3">
<div className="min-w-0 flex-1 basis-64">
<h2 id={`${id}-title`} className="text-xl font-semibold tracking-tight">
{title}
</h2>
{description && <div className="mt-1 max-w-3xl text-body">{description}</div>}
</div>
{action && <div className="flex flex-wrap gap-2">{action}</div>}
</div>
{children && <div className="mt-5">{children}</div>}
</section>
)
}

/** A file input whose picker button looks like the kit's secondary button. */
export const fileInputClass =
"block w-full text-base text-body file:mr-3 file:min-h-11 file:cursor-pointer file:rounded file:border file:border-gray-300 file:bg-white file:px-4 file:py-2 file:font-semibold file:text-ink hover:file:bg-surface"

/** A checkbox or radio with its label, sized for touch. */
export const checkLabelClass = "flex min-h-11 items-center gap-3 text-base text-ink"
export const checkInputClass = "size-5 shrink-0 accent-brand-700"
31 changes: 31 additions & 0 deletions apps/web/src/app/me/_components/tones.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { BadgeTone } from "@/components/ui/Badge"
import type { DonationStatus } from "@/lib/contributions"
import type { FulfillmentStatus } from "@/lib/fulfillment"
import type { RowStatus } from "@/lib/statement-import"

/** Badge tones of the statuses /me lists show (labels stay in lib). */
export const DONATION_TONES: Record<DonationStatus, BadgeTone> = {
awaiting_transfer: "warning",
received: "brand",
expired: "neutral",
}

export const FULFILLMENT_TONES: Record<FulfillmentStatus, BadgeTone> = {
pending: "neutral",
in_progress: "info",
shipped: "info",
delivered: "brand",
issue: "danger",
}

export const ROW_TONES: Record<RowStatus, BadgeTone> = {
ready: "neutral",
duplicate: "neutral",
ambiguous: "warning",
imported: "brand",
}

/** The tone of a status read from the database as a plain string; neutral when unknown. */
export function toneOf(tones: Partial<Record<string, BadgeTone>>, status: string | null): BadgeTone {
return (status !== null && tones[status]) || "neutral"
}
7 changes: 7 additions & 0 deletions apps/web/src/app/me/accounts/error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"use client"

import { MeError } from "../_components/MeError"

export default function ErrorPage({ retry }: { error: unknown; retry: () => void }) {
return <MeError retry={retry} />
}
Loading
Loading