feat: initialize zhixing-web with React, React Router, and Tailwind CSS
- Set up the main application structure with App component and routing. - Implemented global state management for UI theme using Zustand. - Created API utilities for fetching system status. - Developed system status page with responsive design and theme toggle. - Added testing setup with Vitest and initial tests for system status page. - Configured TypeScript with appropriate settings for the application. - Integrated Tailwind CSS for styling and responsive design. - Established Vite configuration for development and testing environments.
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
export class ApiError extends Error {
|
||||
readonly status: number
|
||||
|
||||
constructor(status: number, message: string) {
|
||||
super(message)
|
||||
this.name = "ApiError"
|
||||
this.status = status
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a JSON request through the application's same-origin transport boundary.
|
||||
*
|
||||
* @param input - Same-origin API path or URL.
|
||||
* @param init - Native fetch options such as AbortSignal and method.
|
||||
* @returns The decoded response body typed by the feature adapter.
|
||||
* @throws ApiError when the server returns a non-success response.
|
||||
*/
|
||||
export async function requestJson<T>(
|
||||
input: RequestInfo | URL,
|
||||
init?: RequestInit,
|
||||
): Promise<T> {
|
||||
const headers = new Headers(init?.headers)
|
||||
headers.set("Accept", "application/json")
|
||||
|
||||
const response = await fetch(input, { ...init, headers })
|
||||
|
||||
if (!response.ok) {
|
||||
throw new ApiError(
|
||||
response.status,
|
||||
`API request failed with status ${response.status}`,
|
||||
)
|
||||
}
|
||||
|
||||
return (await response.json()) as T
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { create } from "zustand"
|
||||
import { persist } from "zustand/middleware"
|
||||
|
||||
type Theme = "dark" | "light"
|
||||
|
||||
interface UiState {
|
||||
theme: Theme
|
||||
toggleTheme: () => void
|
||||
}
|
||||
|
||||
export const useUiStore = create<UiState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
theme: "light",
|
||||
toggleTheme: () =>
|
||||
set((state) => ({ theme: state.theme === "light" ? "dark" : "light" })),
|
||||
}),
|
||||
{ name: "zhixing-ui" },
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,28 @@
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import type { HTMLAttributes } from "react"
|
||||
|
||||
import { cn } from "@/shared/ui/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "border-transparent bg-primary text-primary-foreground",
|
||||
destructive: "border-transparent bg-destructive text-white",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: { variant: "default" },
|
||||
},
|
||||
)
|
||||
|
||||
export function Badge({
|
||||
className,
|
||||
variant,
|
||||
...props
|
||||
}: HTMLAttributes<HTMLSpanElement> & VariantProps<typeof badgeVariants>) {
|
||||
return (
|
||||
<span className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import type { ButtonHTMLAttributes } from "react"
|
||||
|
||||
import { cn } from "@/shared/ui/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors disabled:pointer-events-none disabled:opacity-50 [&_svg]:size-4",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
outline:
|
||||
"border bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2",
|
||||
icon: "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: { size: "default", variant: "default" },
|
||||
},
|
||||
)
|
||||
|
||||
export function Button({
|
||||
className,
|
||||
size,
|
||||
type = "button",
|
||||
variant,
|
||||
...props
|
||||
}: ButtonHTMLAttributes<HTMLButtonElement> &
|
||||
VariantProps<typeof buttonVariants>) {
|
||||
return (
|
||||
<button
|
||||
className={cn(buttonVariants({ size, variant }), className)}
|
||||
type={type}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { HTMLAttributes } from "react"
|
||||
|
||||
import { cn } from "@/shared/ui/utils"
|
||||
|
||||
export function Card({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-xl border bg-card text-card-foreground shadow",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function CardHeader({
|
||||
className,
|
||||
...props
|
||||
}: HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function CardTitle({
|
||||
className,
|
||||
...props
|
||||
}: HTMLAttributes<HTMLHeadingElement>) {
|
||||
return (
|
||||
<h1
|
||||
className={cn("font-semibold leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function CardDescription({
|
||||
className,
|
||||
...props
|
||||
}: HTMLAttributes<HTMLParagraphElement>) {
|
||||
return (
|
||||
<p className={cn("text-sm text-muted-foreground", className)} {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
export function CardContent({
|
||||
className,
|
||||
...props
|
||||
}: HTMLAttributes<HTMLDivElement>) {
|
||||
return <div className={cn("p-6 pt-0", className)} {...props} />
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
Reference in New Issue
Block a user