mirror of
https://gitlab.com/megazordpobeda/DataRush.git
synced 2026-05-23 05:07:10 +00:00
feat: created competitions page model
This commit is contained in:
@@ -1,6 +1,14 @@
|
||||
import { Routes } from "react-router";
|
||||
import { Routes, Route } from "react-router";
|
||||
import "./styles/globals.css";
|
||||
import CompetitionsPage from "./pages/CompetitionsPage";
|
||||
|
||||
export default function App() {
|
||||
return <Routes />;
|
||||
}
|
||||
const App = () => {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/" element={<CompetitionsPage/>} />
|
||||
</Routes>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,66 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/shared/lib/utils"
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-background text-foreground",
|
||||
destructive:
|
||||
"text-destructive-foreground [&>svg]:text-current *:data-[slot=alert-description]:text-destructive-foreground/80",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Alert({
|
||||
className,
|
||||
variant,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert"
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-title"
|
||||
className={cn(
|
||||
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-description"
|
||||
className={cn(
|
||||
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription }
|
||||
@@ -0,0 +1,46 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/shared/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40",
|
||||
outline:
|
||||
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"span"> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "span"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="badge"
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
@@ -0,0 +1,68 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/shared/lib/utils"
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"bg-card text-card-foreground flex flex-col rounded-xl border shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn("flex flex-col gap-1.5 px-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn("leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn("flex items-center px-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||
@@ -0,0 +1,13 @@
|
||||
import { cn } from "@/shared/lib/utils"
|
||||
|
||||
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="skeleton"
|
||||
className={cn("bg-primary/10 animate-pulse rounded-md", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Skeleton }
|
||||
@@ -0,0 +1,70 @@
|
||||
import * as React from "react"
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||
|
||||
import { cn } from "@/shared/lib/utils"
|
||||
|
||||
function Tabs({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
||||
return (
|
||||
<TabsPrimitive.Root
|
||||
data-slot="tabs"
|
||||
className={cn("flex flex-col", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsList({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.List>) {
|
||||
return (
|
||||
<TabsPrimitive.List
|
||||
data-slot="tabs-list"
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center gap-6",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsTrigger({
|
||||
className,
|
||||
value,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Trigger> & { value: string }) {
|
||||
return (
|
||||
<TabsPrimitive.Trigger
|
||||
data-slot="tabs-trigger"
|
||||
value={value}
|
||||
className={cn(
|
||||
"relative px-1 py-2 text-sm font-medium outline-none",
|
||||
"text-gray-500",
|
||||
"data-[state=active]:font-semibold after:absolute after:bottom-0 after:left-0 after:right-0 after:h-0.5",
|
||||
value === "ongoing" && "data-[state=active]:text-yellow-500 data-[state=active]:after:bg-yellow-500",
|
||||
value === "completed" && "data-[state=active]:text-green-500 data-[state=active]:after:bg-green-500",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
||||
return (
|
||||
<TabsPrimitive.Content
|
||||
data-slot="tabs-content"
|
||||
className={cn("mt-2 outline-none", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Competition } from "../../types";
|
||||
import { cn } from "@/shared/lib/utils";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
} from "@/components/ui/card";
|
||||
|
||||
interface CompetitionCardProps {
|
||||
competition: Competition;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function CompetitionCard({ competition, className }: CompetitionCardProps) {
|
||||
const { name, imageUrl, isOlympics, status } = competition;
|
||||
|
||||
return (
|
||||
<Card className={cn("overflow-hidden h-full", className)}>
|
||||
<div className="relative h-48 overflow-hidden">
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt={name}
|
||||
className="w-full h-full object-cover transition-transform duration-300 hover:scale-105"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CardFooter className="p-4 pb-0 flex items-center text-xs font-medium font-hse-sans">
|
||||
<span className="text-gray-500">
|
||||
{isOlympics ? "Олимпиада" : "Тренировка"}
|
||||
</span>
|
||||
<span className="mx-2 w-1.5 h-1.5 rounded-full bg-gray-300"></span>
|
||||
<span className={cn(
|
||||
status === 'В процессе' && "text-yellow-500",
|
||||
status === 'Завершено' && "text-green-500",
|
||||
status === 'Не участвую' && "text-gray-500"
|
||||
)}>
|
||||
{status.replace(/^\w/, c => c.toUpperCase())}
|
||||
</span>
|
||||
</CardFooter>
|
||||
|
||||
<CardContent className="p-4 pt-2">
|
||||
<h3 className="font-semibold text-lg line-clamp-2 font-hse-sans">{name}</h3>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Card, CardContent, CardFooter } from "@/components/ui/card";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
const CompetitionSkeleton = () => {
|
||||
return (
|
||||
<Card className="overflow-hidden">
|
||||
<Skeleton className="h-48 w-full" />
|
||||
<CardContent className="p-4 pt-5">
|
||||
<Skeleton className="h-6 w-3/4 mb-2" />
|
||||
<Skeleton className="h-6 w-1/2" />
|
||||
</CardContent>
|
||||
<CardFooter className="p-4 pt-0 flex gap-2">
|
||||
<Skeleton className="h-6 w-20" />
|
||||
<Skeleton className="h-6 w-24" />
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default CompetitionSkeleton
|
||||
@@ -0,0 +1,26 @@
|
||||
import { cn } from "@/shared/lib/utils";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
interface CompetitionTagProps {
|
||||
label: string;
|
||||
variant: 'olympics' | 'status';
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const CompetitionTag = ({ label, variant, className }: CompetitionTagProps) => {
|
||||
return (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className={cn(
|
||||
"text-xs font-medium",
|
||||
variant === 'olympics' && "bg-yellow-400 text-yellow-800 hover:bg-yellow-500 font-hse-sans",
|
||||
variant === 'status' && "bg-black text-white hover:bg-gray-800 font-hse-sans",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
export default CompetitionTag
|
||||
@@ -0,0 +1,143 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Competition, Status } from './types';
|
||||
import { CompetitionGrid } from './modules/CompetitionGrid';
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import Navbar from './modules/Navbar';
|
||||
|
||||
const mockCompetitions: Competition[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Олимпиада DANO 2025. Индивидуальный этап',
|
||||
imageUrl: '/DANO.png',
|
||||
isOlympics: true,
|
||||
status: Status.InProgress
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Олимпиада DANO 2025. Индивидуальный этап',
|
||||
imageUrl: '/DANO.png',
|
||||
isOlympics: false,
|
||||
status: Status.NotParticipating
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Олимпиада DANO 2025. Индивидуальный этап',
|
||||
imageUrl: '/DANO.png',
|
||||
isOlympics: false,
|
||||
status: Status.InProgress
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'Олимпиада DANO 2025. Индивидуальный этап',
|
||||
imageUrl: '/DANO.png',
|
||||
isOlympics: true,
|
||||
status: Status.Completed
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: 'Олимпиада DANO 2025. Индивидуальный этап',
|
||||
imageUrl: '/DANO.png',
|
||||
isOlympics: false,
|
||||
status: Status.Completed
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
name: 'Олимпиада DANO 2025. Индивидуальный этап',
|
||||
imageUrl: '/DANO.png',
|
||||
isOlympics: true,
|
||||
status: Status.NotParticipating
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
const CompetitionsPage = () => {
|
||||
const [competitions, setCompetitions] = useState<Competition[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [activeTab, setActiveTab] = useState("ongoing");
|
||||
|
||||
useEffect(() => {
|
||||
// ! симуляция фетча
|
||||
const fetchCompetitions = async () => {
|
||||
try {
|
||||
setTimeout(() => {
|
||||
setCompetitions(mockCompetitions);
|
||||
setIsLoading(false);
|
||||
}, 800);
|
||||
} catch (error) {
|
||||
setError('Соревнования не найдены, пожалуйста, попробуйте позже');
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchCompetitions();
|
||||
}, []);
|
||||
|
||||
const myCompetitions = competitions.filter(comp =>
|
||||
comp.status === Status.InProgress || comp.status === Status.Completed
|
||||
);
|
||||
|
||||
const filteredMyCompetitions = myCompetitions.filter(comp =>
|
||||
activeTab === "ongoing" ? comp.status === Status.InProgress : comp.status === Status.Completed
|
||||
);
|
||||
|
||||
const availableCompetitions = competitions.filter(comp =>
|
||||
comp.status === 'Не участвую'
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<div className="container mx-auto px-4 py-8 mt-16">
|
||||
{error && (
|
||||
<Alert variant="destructive" className="mb-6">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="mb-12">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-2xl font-semibold font-hse-sans">Мои события</h2>
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-auto">
|
||||
<TabsList>
|
||||
<TabsTrigger value="ongoing" className="font-hse-sans">Текущие</TabsTrigger>
|
||||
<TabsTrigger value="completed" className="font-hse-sans">Завершенные</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<CompetitionGrid competitions={[]} isLoading={true} />
|
||||
) : filteredMyCompetitions.length > 0 ? (
|
||||
<CompetitionGrid competitions={filteredMyCompetitions} isLoading={false} />
|
||||
) : (
|
||||
<div className="flex justify-center items-center h-40 bg-gray-50 rounded-lg">
|
||||
<p className="text-gray-500 font-hse-sans">
|
||||
{activeTab === "ongoing" ? "У вас нет текущих соревнований" : "У вас нет завершенных соревнований"}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold mb-6 font-hse-sans">Доступные события</h2>
|
||||
|
||||
{isLoading ? (
|
||||
<CompetitionGrid competitions={[]} isLoading={true} />
|
||||
) : availableCompetitions.length > 0 ? (
|
||||
<CompetitionGrid competitions={availableCompetitions} isLoading={false} />
|
||||
) : (
|
||||
<div className="flex justify-center items-center h-40 bg-gray-50 rounded-lg">
|
||||
<p className="text-gray-500 font-hse-sans">Нет доступных соревнований</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default CompetitionsPage;
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Competition } from "../../types";
|
||||
import { CompetitionCard } from "../../components/CompetitionCard";
|
||||
import CompetitionSkeleton from "../../components/CompetitionSkeleton";
|
||||
import { cn } from "@/shared/lib/utils";
|
||||
|
||||
interface CompetitionGridProps {
|
||||
competitions: Competition[];
|
||||
isLoading?: boolean;
|
||||
className?: string;
|
||||
skeletonCount?: number;
|
||||
}
|
||||
|
||||
export function CompetitionGrid({
|
||||
competitions,
|
||||
isLoading = false,
|
||||
className,
|
||||
skeletonCount
|
||||
}: CompetitionGridProps) {
|
||||
const gridClasses = cn(
|
||||
"grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6",
|
||||
className
|
||||
);
|
||||
|
||||
const numberOfSkeletons = skeletonCount ?? (competitions.length > 0 ? competitions.length : 4);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className={gridClasses}>
|
||||
{Array.from({ length: numberOfSkeletons }).map((_, index) => (
|
||||
<CompetitionSkeleton key={index} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={gridClasses}>
|
||||
{competitions.map((competition) => (
|
||||
<CompetitionCard
|
||||
key={competition.id}
|
||||
competition={competition}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ChevronDown } from "lucide-react";
|
||||
|
||||
const Navbar = () => {
|
||||
return (
|
||||
<nav className="bg-white border-b border-gray-200 py-3 px-4 fixed top-0 left-0 right-0 z-10">
|
||||
<div className="container mx-auto flex justify-between items-center">
|
||||
<div className="flex items-center">
|
||||
<div className="bg-black px-3 py-2 rounded font-hse-sans">
|
||||
<span className="font-bold text-yellow-400">DATA</span>
|
||||
<span className="font-bold text-white">RUSH</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center cursor-pointer">
|
||||
<span className="mr-2 font-semibold font-hse-sans">itqdev</span>
|
||||
<ChevronDown size={16} />
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default Navbar
|
||||
@@ -0,0 +1,13 @@
|
||||
export enum Status {
|
||||
InProgress = 'В процессе',
|
||||
NotParticipating = 'Не участвую',
|
||||
Completed = 'Завершено'
|
||||
}
|
||||
|
||||
export interface Competition {
|
||||
id: string;
|
||||
name: string;
|
||||
imageUrl: string;
|
||||
isOlympics: boolean;
|
||||
status: Status;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
@font-face {
|
||||
font-family: 'HSE Sans';
|
||||
src: url('/fonts/HSESans-Regular.otf') format('opentype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'HSE Sans';
|
||||
src: url('/fonts/HSESans-Bold.otf') format('opentype');
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'HSE Sans';
|
||||
src: url('/fonts/HSESans-SemiBold.otf') format('opentype');
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@import "./fonts.css";
|
||||
@plugin "tailwindcss-animate";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
@@ -40,6 +40,9 @@
|
||||
--sidebar-ring: oklch(0.87 0 0);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--font-hse-sans: "HSE Sans", system-ui, sans-serif
|
||||
}
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
|
||||
Reference in New Issue
Block a user