add shadcn ui

This commit is contained in:
Nông Đức Huy
2026-08-13 23:20:23 +07:00
parent 57f44796ce
commit fee9523b35
50 changed files with 1331 additions and 211 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
import createNextIntlPlugin from 'next-intl/plugin';
import type { NextConfig } from 'next';
import createNextIntlPlugin from 'next-intl/plugin';
/** Registers the cookie-based locale resolver in src/i18n/request.ts. */
const withNextIntl = createNextIntlPlugin('./src/i18n/request.ts');
+2 -6
View File
@@ -5,7 +5,7 @@ import { useTranslations } from 'next-intl';
import { useEffect, useState, type FormEvent } from 'react';
import { isApiClientError } from '@sport/api-client';
import { Button, Input } from '@sport/ui';
import { Button, FormError, Input } from '@sport/ui';
import { loginSchema } from '@sport/validation';
import { useSession } from './session-provider';
@@ -91,11 +91,7 @@ export function LoginForm() {
/>
</div>
{error ? (
<p role="alert" className="text-danger text-sm">
{error}
</p>
) : null}
<FormError message={error} />
<Button type="submit" fullWidth size="lg" disabled={submitting}>
{submitting ? t('signingIn') : t('signIn')}
@@ -13,7 +13,7 @@ import {
type ContentType,
type Locale,
} from '@sport/types';
import { Badge, Button, Input, Skeleton, cn } from '@sport/ui';
import { Badge, Button, FormError, Input, Skeleton, Textarea, cn } from '@sport/ui';
import { browserApi } from '@/lib/api';
@@ -275,12 +275,11 @@ export function ContentEditor({
<label className="block space-y-1.5">
<span className="text-xs font-semibold uppercase tracking-widest">{t('body')}</span>
<textarea
<Textarea
value={fields.body}
onChange={(event) => setField('body', event.target.value)}
rows={22}
spellCheck
className="border-ink-200 focus:border-ink-950 w-full border px-3 py-2 font-mono text-sm leading-relaxed outline-none"
/>
<span className="text-ink-500 text-xs">{t('bodyHint')}</span>
</label>
@@ -313,11 +312,7 @@ export function ContentEditor({
</div>
</details>
{error ? (
<p role="alert" className="text-danger text-sm">
{error}
</p>
) : null}
<FormError message={error} />
<div className="border-ink-200 flex flex-wrap items-center gap-2 border-t pt-4">
<Button type="submit" variant="outline" disabled={saving}>
@@ -17,6 +17,7 @@ import {
DataTableHeader,
DataTableRow,
EmptyState,
FormError,
Input,
SegmentedControl,
Skeleton,
@@ -107,11 +108,7 @@ export function ContentTable() {
) : null}
</div>
{error ? (
<p role="alert" className="text-danger text-sm">
{error}
</p>
) : null}
<FormError message={error} />
{!entries ? (
<div className="space-y-2" aria-busy="true">
@@ -20,6 +20,7 @@ import {
DialogHeader,
DialogTitle,
EmptyState,
FormError,
Input,
Skeleton,
} from '@sport/ui';
@@ -77,11 +78,7 @@ export function CustomersTable() {
aria-label={t('searchPlaceholder')}
/>
{error ? (
<p role="alert" className="text-danger text-sm">
{error}
</p>
) : null}
<FormError message={error} />
{!customers ? (
<div className="space-y-2" aria-busy="true">
@@ -7,12 +7,14 @@ import { isApiClientError, type DiscountPayload } from '@sport/api-client';
import { DEFAULT_LOCALE, LOCALES, type AdminDiscount, type Locale } from '@sport/types';
import {
Button,
Checkbox,
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
FormError,
Input,
cn,
} from '@sport/ui';
@@ -356,11 +358,7 @@ export function DiscountDialog({
</div>
</div>
{error ? (
<p role="alert" className="text-danger text-sm">
{error}
</p>
) : null}
<FormError message={error} />
</form>
<DialogFooter className="sm:justify-between">
@@ -458,12 +456,10 @@ function Toggle({
return (
<div className="flex items-start gap-2.5">
<input
<Checkbox
id={id}
type="checkbox"
checked={checked}
onChange={(event) => onChange(event.target.checked)}
className="border-ink-300 accent-ink-950 mt-0.5 size-4"
onCheckedChange={(checked) => onChange(checked === true)}
/>
<div className="space-y-0.5">
<label htmlFor={id} className="block text-sm font-medium">
@@ -16,6 +16,7 @@ import {
DataTableHeader,
DataTableRow,
EmptyState,
FormError,
Input,
SegmentedControl,
Skeleton,
@@ -114,11 +115,7 @@ export function DiscountsTable() {
</Button>
</div>
{error ? (
<p role="alert" className="text-danger text-sm">
{error}
</p>
) : null}
<FormError message={error} />
{!discounts ? (
<div className="space-y-2" aria-busy="true">
@@ -14,6 +14,7 @@ import {
DataTableHead,
DataTableHeader,
DataTableRow,
FormError,
Input,
Skeleton,
cn,
@@ -106,11 +107,7 @@ export function InventoryTable() {
</label>
</div>
{error ? (
<p role="alert" className="text-danger text-sm">
{error}
</p>
) : null}
<FormError message={error} />
{!levels ? (
<div className="space-y-2" aria-busy="true">
@@ -6,7 +6,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
import { isApiClientError } from '@sport/api-client';
import { PERMISSIONS, type MediaAssetSummary } from '@sport/types';
import { Button, Skeleton } from '@sport/ui';
import { Button, FormError, Skeleton } from '@sport/ui';
import { useSession } from '@/features/auth/session-provider';
import { browserApi } from '@/lib/api';
@@ -145,11 +145,7 @@ export function MediaLibrary() {
</ul>
) : null}
{error ? (
<p role="alert" className="text-danger text-sm">
{error}
</p>
) : null}
<FormError message={error} />
{!assets ? (
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4 lg:grid-cols-6" aria-busy="true">
@@ -6,7 +6,7 @@ import { useEffect, useState } from 'react';
import { isApiClientError } from '@sport/api-client';
import { PERMISSIONS, type Order, type OrderStatus } from '@sport/types';
import { Badge, Button, Input, Skeleton } from '@sport/ui';
import { Badge, Button, FormError, Input, Skeleton } from '@sport/ui';
import { useSession } from '@/features/auth/session-provider';
import { browserApi } from '@/lib/api';
@@ -103,11 +103,7 @@ export function OrderDetail({ orderId }: { orderId: string }) {
<Badge variant="outline">{t(`payment.${order.paymentStatus}`)}</Badge>
</header>
{error ? (
<p role="alert" className="text-danger text-sm">
{error}
</p>
) : null}
<FormError message={error} />
{can(PERMISSIONS.ORDER_UPDATE) && next.length > 0 ? (
<section className="border-ink-200 space-y-3 border bg-white p-6">
@@ -15,6 +15,7 @@ import {
DataTableHead,
DataTableHeader,
DataTableRow,
FormError,
Input,
Skeleton,
cn,
@@ -115,11 +116,7 @@ export function OrdersTable() {
<span className="text-ink-500 ml-auto text-xs">{t('count', { count: total })}</span>
</div>
{error ? (
<p role="alert" className="text-danger text-sm">
{error}
</p>
) : null}
<FormError message={error} />
{!orders ? (
<div className="space-y-2" aria-busy="true">
@@ -11,7 +11,7 @@ import {
type AdminProductDetail,
type MediaAssetSummary,
} from '@sport/types';
import { Button, Skeleton, cn } from '@sport/ui';
import { Button, FormError, Skeleton, cn } from '@sport/ui';
import { useSession } from '@/features/auth/session-provider';
import { browserApi } from '@/lib/api';
@@ -139,11 +139,7 @@ export function ImageManager({
) : null}
</div>
{error ? (
<p role="alert" className="text-danger text-sm">
{error}
</p>
) : null}
<FormError message={error} />
{slots.length === 0 ? (
<p className="text-ink-500 text-sm">{t('noImages')}</p>
@@ -6,7 +6,7 @@ import { useEffect, useState } from 'react';
import { isApiClientError } from '@sport/api-client';
import { PERMISSIONS, type AdminProductDetail } from '@sport/types';
import { Badge, Button, Skeleton, cn } from '@sport/ui';
import { Badge, Button, FormError, Skeleton, cn } from '@sport/ui';
import { useSession } from '@/features/auth/session-provider';
import { browserApi } from '@/lib/api';
@@ -127,11 +127,7 @@ export function ProductEditor({ productId }: { productId: string }) {
) : null}
</header>
{error ? (
<p role="alert" className="text-danger text-sm">
{error}
</p>
) : null}
<FormError message={error} />
<nav className="border-ink-200 flex gap-1 border-b">
{(['details', 'variants', 'images'] as const).map((value) => (
@@ -12,7 +12,7 @@ import {
type AdminProductDetail,
type Locale,
} from '@sport/types';
import { Badge, Button, Input, cn } from '@sport/ui';
import { Badge, Button, FormError, Input, Textarea, cn } from '@sport/ui';
import { browserApi } from '@/lib/api';
@@ -264,7 +264,7 @@ export function ProductForm({
<span className="text-xs font-semibold uppercase tracking-widest">
{t('description')}
</span>
<textarea
<Textarea
rows={5}
value={translations[locale].description}
onChange={(event) =>
@@ -347,11 +347,7 @@ export function ProductForm({
</section>
) : null}
{error ? (
<p role="alert" className="text-danger text-sm">
{error}
</p>
) : null}
<FormError message={error} />
<div className="flex items-center gap-3">
<Button size="lg" disabled={saving} onClick={() => void save()}>
@@ -16,6 +16,7 @@ import {
DataTableHead,
DataTableHeader,
DataTableRow,
FormError,
Input,
Skeleton,
cn,
@@ -141,11 +142,7 @@ export function ProductsTable() {
) : null}
</div>
{error ? (
<p role="alert" className="text-danger text-sm">
{error}
</p>
) : null}
<FormError message={error} />
{!items ? (
<div className="space-y-2" aria-busy="true">
@@ -5,7 +5,7 @@ import { useState } from 'react';
import { isApiClientError, type VariantPatch } from '@sport/api-client';
import { PERMISSIONS, type AdminProductDetail, type AdminProductVariant } from '@sport/types';
import { Badge, Button, Input, cn } from '@sport/ui';
import { Badge, Button, FormError, Input, cn } from '@sport/ui';
import { useSession } from '@/features/auth/session-provider';
import { browserApi } from '@/lib/api';
@@ -131,11 +131,7 @@ export function VariantGrid({
</span>
</div>
{error ? (
<p role="alert" className="text-danger text-sm">
{error}
</p>
) : null}
<FormError message={error} />
<div className="border-ink-200 overflow-x-auto border bg-white">
<table className="min-w-3xl w-full text-sm">
@@ -5,7 +5,16 @@ import { useCallback, useEffect, useState } from 'react';
import { isApiClientError } from '@sport/api-client';
import { PERMISSIONS, type AdminReview, type ReviewStatus } from '@sport/types';
import { Badge, Button, EmptyState, Input, Rating, SegmentedControl, Skeleton } from '@sport/ui';
import {
Badge,
Button,
EmptyState,
FormError,
Input,
Rating,
SegmentedControl,
Skeleton,
} from '@sport/ui';
import { useSession } from '@/features/auth/session-provider';
import { browserApi } from '@/lib/api';
@@ -97,11 +106,7 @@ export function ReviewsQueue() {
/>
</div>
{error ? (
<p role="alert" className="text-danger text-sm">
{error}
</p>
) : null}
<FormError message={error} />
{!reviews ? (
<div className="space-y-2" aria-busy="true">
@@ -23,6 +23,7 @@ import {
DialogTitle,
EmptyState,
Field,
FormError,
Input,
Skeleton,
cn,
@@ -171,11 +172,7 @@ export function TaxonomyManager<TRow extends TaxonomyRow>({
</div>
) : null}
{error ? (
<p role="alert" className="text-danger text-sm">
{error}
</p>
) : null}
<FormError message={error} />
{!rows ? (
<div className="space-y-2" aria-busy="true">
@@ -437,11 +434,7 @@ function TaxonomyDialog<TRow extends TaxonomyRow>({
</div>
</div>
{error ? (
<p role="alert" className="text-danger text-sm">
{error}
</p>
) : null}
<FormError message={error} />
</form>
<DialogFooter className="sm:justify-between">
+1 -2
View File
@@ -1,9 +1,8 @@
{
"extends": "@sport/config/typescript/nextjs.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
"@/*": ["./src/*"]
}
},
"include": [