feat: добавлены анимации загрузки и переходов между разделами
- Добавлен полноэкранный индикатор загрузки с размытием фона (LoadingOverlay) - Реализован глобальный контекст загрузки для отслеживания переходов между страницами - Добавлены плавные fade-анимации при переходах между страницами - Реализованы поочередные (stagger) анимации для карточек: * Карточки дней на странице расписания * Карточки уроков внутри каждого дня * Карточки групп на главной странице - Анимации работают на всех устройствах, включая мобильные - Улучшен компонент Spinner с поддержкой разных размеров - Исправлена ошибка гидратации с вложенными <li> элементами в навигации - Оптимизированы задержки анимаций для более быстрого отображения контента
This commit is contained in:
@@ -1,9 +1,25 @@
|
|||||||
import '@/shared/styles/globals.css'
|
import '@/shared/styles/globals.css'
|
||||||
import type { AppProps } from 'next/app'
|
import type { AppProps } from 'next/app'
|
||||||
import { ThemeProvider } from '@/shared/providers/theme-provider'
|
import { ThemeProvider } from '@/shared/providers/theme-provider'
|
||||||
|
import { LoadingContextProvider, LoadingContext } from '@/shared/context/loading-context'
|
||||||
|
import { LoadingOverlay } from '@/shared/ui/loading-overlay'
|
||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
export default function App({ Component, pageProps }: AppProps) {
|
function AppContent({ Component, pageProps }: AppProps) {
|
||||||
|
const { isLoading } = React.useContext(LoadingContext)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="page-transition-wrapper">
|
||||||
|
<Component {...pageProps} />
|
||||||
|
</div>
|
||||||
|
<LoadingOverlay isLoading={isLoading} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function App(props: AppProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
@@ -15,7 +31,9 @@ export default function App({ Component, pageProps }: AppProps) {
|
|||||||
enableSystem
|
enableSystem
|
||||||
disableTransitionOnChange
|
disableTransitionOnChange
|
||||||
>
|
>
|
||||||
<Component {...pageProps} />
|
<LoadingContextProvider>
|
||||||
|
<AppContent {...props} />
|
||||||
|
</LoadingContextProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -30,6 +30,17 @@ export default function HomePage({ groups, groupsByCourse }: HomePageProps) {
|
|||||||
const [openCourses, setOpenCourses] = React.useState<Set<number>>(new Set([1]))
|
const [openCourses, setOpenCourses] = React.useState<Set<number>>(new Set([1]))
|
||||||
const [addGroupDialogOpen, setAddGroupDialogOpen] = React.useState(false)
|
const [addGroupDialogOpen, setAddGroupDialogOpen] = React.useState(false)
|
||||||
|
|
||||||
|
// Подсчитываем смещения для каждого курса для последовательной анимации
|
||||||
|
const courseOffsets = React.useMemo(() => {
|
||||||
|
const offsets: { [course: number]: number } = {}
|
||||||
|
let totalGroups = 0
|
||||||
|
for (const course of [1, 2, 3, 4, 5]) {
|
||||||
|
offsets[course] = totalGroups
|
||||||
|
totalGroups += (groupsByCourse[course] || []).length
|
||||||
|
}
|
||||||
|
return { offsets, totalGroups }
|
||||||
|
}, [groupsByCourse])
|
||||||
|
|
||||||
const toggleCourse = (course: number) => {
|
const toggleCourse = (course: number) => {
|
||||||
setOpenCourses(prev => {
|
setOpenCourses(prev => {
|
||||||
const next = new Set(prev)
|
const next = new Set(prev)
|
||||||
@@ -50,55 +61,80 @@ export default function HomePage({ groups, groupsByCourse }: HomePageProps) {
|
|||||||
</Head>
|
</Head>
|
||||||
<div className="min-h-screen p-4 md:p-8">
|
<div className="min-h-screen p-4 md:p-8">
|
||||||
<div className="max-w-4xl mx-auto space-y-4">
|
<div className="max-w-4xl mx-auto space-y-4">
|
||||||
<div className="text-center space-y-2 mb-8">
|
<div className="text-center space-y-2 mb-8 stagger-card" style={{ animationDelay: '0.05s' } as React.CSSProperties}>
|
||||||
<h1 className="text-3xl md:text-4xl font-bold">Расписание занятий</h1>
|
<h1 className="text-3xl md:text-4xl font-bold">Расписание занятий</h1>
|
||||||
<p className="text-muted-foreground">Выберите группу для просмотра расписания</p>
|
<p className="text-muted-foreground">Выберите группу для просмотра расписания</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{[1, 2, 3, 4, 5].map(course => {
|
{[1, 2, 3, 4, 5].map((course, courseIndex) => {
|
||||||
const courseGroups = groupsByCourse[course] || []
|
const courseGroups = groupsByCourse[course] || []
|
||||||
const isOpen = openCourses.has(course)
|
const isOpen = openCourses.has(course)
|
||||||
|
const courseOffset = courseOffsets.offsets[course]
|
||||||
|
|
||||||
if (courseGroups.length === 0) {
|
if (courseGroups.length === 0) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card key={course}>
|
<div
|
||||||
<CardHeader
|
key={course}
|
||||||
className="cursor-pointer"
|
className="stagger-card"
|
||||||
onClick={() => toggleCourse(course)}
|
style={{
|
||||||
>
|
animationDelay: `${0.1 + courseIndex * 0.05}s`,
|
||||||
<div className="flex items-center justify-between">
|
} as React.CSSProperties}
|
||||||
<CardTitle className="text-xl">
|
>
|
||||||
{course} курс
|
<Card>
|
||||||
</CardTitle>
|
<CardHeader
|
||||||
<ChevronDown
|
className="cursor-pointer"
|
||||||
className={cn(
|
onClick={() => toggleCourse(course)}
|
||||||
"h-5 w-5 transition-transform duration-200",
|
>
|
||||||
isOpen && "transform rotate-180"
|
<div className="flex items-center justify-between">
|
||||||
)}
|
<CardTitle className="text-xl">
|
||||||
/>
|
{course} курс
|
||||||
</div>
|
</CardTitle>
|
||||||
</CardHeader>
|
<ChevronDown
|
||||||
{isOpen && (
|
className={cn(
|
||||||
<CardContent>
|
"h-5 w-5 transition-transform duration-200",
|
||||||
<div className="grid grid-cols-3 sm:grid-cols-2 lg:grid-cols-3 gap-2">
|
isOpen && "transform rotate-180"
|
||||||
{courseGroups.map(({ id, name }) => (
|
)}
|
||||||
<Link key={id} href={`/${id}`}>
|
/>
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="w-full justify-center h-auto py-3 px-2 sm:px-4 text-sm sm:text-base whitespace-nowrap"
|
|
||||||
>
|
|
||||||
{name}
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardHeader>
|
||||||
)}
|
{isOpen && (
|
||||||
</Card>
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-3 sm:grid-cols-2 lg:grid-cols-3 gap-2">
|
||||||
|
{courseGroups.map(({ id, name }, groupIndex) => {
|
||||||
|
// Последовательная анимация: каждый следующий элемент с задержкой
|
||||||
|
// courseOffset - это количество групп во всех предыдущих курсах
|
||||||
|
// groupIndex - это индекс в текущем курсе
|
||||||
|
// Итого: последовательный счетчик для всех групп подряд
|
||||||
|
const globalIndex = courseOffset + groupIndex
|
||||||
|
const delay = 0.15 + globalIndex * 0.04
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={id}
|
||||||
|
className="stagger-card"
|
||||||
|
style={{
|
||||||
|
animationDelay: `${delay}s`,
|
||||||
|
} as React.CSSProperties}
|
||||||
|
>
|
||||||
|
<Link href={`/${id}`}>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full justify-center h-auto py-3 px-2 sm:px-4 text-sm sm:text-base whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
@@ -112,26 +148,39 @@ export default function HomePage({ groups, groupsByCourse }: HomePageProps) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex flex-col sm:flex-row items-center justify-center gap-3 mt-8">
|
<div className="flex flex-col sm:flex-row items-center justify-center gap-3 mt-8">
|
||||||
<Button
|
<div
|
||||||
variant="secondary"
|
className="stagger-card"
|
||||||
onClick={() => setAddGroupDialogOpen(true)}
|
style={{ animationDelay: `${0.15 + courseOffsets.totalGroups * 0.04 + 0.05}s` } as React.CSSProperties}
|
||||||
className="gap-2"
|
>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => setAddGroupDialogOpen(true)}
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
<MdAdd className="h-4 w-4" />
|
||||||
|
Добавить группу
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="relative stagger-card"
|
||||||
|
style={{ animationDelay: `${0.15 + courseOffsets.totalGroups * 0.04 + 0.08}s` } as React.CSSProperties}
|
||||||
>
|
>
|
||||||
<MdAdd className="h-4 w-4" />
|
|
||||||
Добавить группу
|
|
||||||
</Button>
|
|
||||||
<div className="relative">
|
|
||||||
<ThemeSwitcher />
|
<ThemeSwitcher />
|
||||||
<span className="absolute -bottom-5 left-1/2 transform -translate-x-1/2 text-xs text-muted-foreground whitespace-nowrap sm:hidden">
|
<span className="absolute -bottom-5 left-1/2 transform -translate-x-1/2 text-xs text-muted-foreground whitespace-nowrap sm:hidden">
|
||||||
Тема
|
Тема
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Link href={GITHUB_REPO_URL} target="_blank" rel="noopener noreferrer">
|
<div
|
||||||
<Button variant="outline" className="gap-2">
|
className="stagger-card"
|
||||||
<FaGithub className="h-4 w-4" />
|
style={{ animationDelay: `${0.15 + courseOffsets.totalGroups * 0.04 + 0.11}s` } as React.CSSProperties}
|
||||||
GitHub
|
>
|
||||||
</Button>
|
<Link href={GITHUB_REPO_URL} target="_blank" rel="noopener noreferrer">
|
||||||
</Link>
|
<Button variant="outline" className="gap-2">
|
||||||
|
<FaGithub className="h-4 w-4" />
|
||||||
|
GitHub
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
46
src/shared/context/loading-context.tsx
Normal file
46
src/shared/context/loading-context.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
|
||||||
|
interface LoadingContextType {
|
||||||
|
isLoading: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LoadingContext = React.createContext<LoadingContextType>({
|
||||||
|
isLoading: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
export function LoadingContextProvider({ children }: React.PropsWithChildren) {
|
||||||
|
const [isLoading, setIsLoading] = React.useState(false)
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const handleRouteChangeStart = () => {
|
||||||
|
setIsLoading(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRouteChangeComplete = () => {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRouteChangeError = () => {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
router.events.on('routeChangeStart', handleRouteChangeStart)
|
||||||
|
router.events.on('routeChangeComplete', handleRouteChangeComplete)
|
||||||
|
router.events.on('routeChangeError', handleRouteChangeError)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
router.events.off('routeChangeStart', handleRouteChangeStart)
|
||||||
|
router.events.off('routeChangeComplete', handleRouteChangeComplete)
|
||||||
|
router.events.off('routeChangeError', handleRouteChangeError)
|
||||||
|
}
|
||||||
|
}, [router])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LoadingContext.Provider value={{ isLoading }}>
|
||||||
|
{children}
|
||||||
|
</LoadingContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@@ -146,4 +146,37 @@
|
|||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Page transition animations */
|
||||||
|
@layer utilities {
|
||||||
|
.page-transition-wrapper {
|
||||||
|
animation: fadeIn 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stagger animation for cards - works on all devices */
|
||||||
|
.stagger-card {
|
||||||
|
animation: fadeInSlideUp 0.3s ease-out forwards;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInSlideUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
32
src/shared/ui/loading-overlay.tsx
Normal file
32
src/shared/ui/loading-overlay.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Spinner } from './spinner'
|
||||||
|
import { cn } from '@/shared/utils'
|
||||||
|
|
||||||
|
interface LoadingOverlayProps {
|
||||||
|
isLoading: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LoadingOverlay({ isLoading }: LoadingOverlayProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'fixed inset-0 z-50 flex items-center justify-center',
|
||||||
|
'bg-background/80 backdrop-blur-md',
|
||||||
|
'transition-opacity duration-300',
|
||||||
|
isLoading ? 'opacity-100' : 'opacity-0 pointer-events-none'
|
||||||
|
)}
|
||||||
|
aria-label="Загрузка"
|
||||||
|
role="status"
|
||||||
|
aria-hidden={!isLoading}
|
||||||
|
>
|
||||||
|
{isLoading && (
|
||||||
|
<div className="flex flex-col items-center gap-4">
|
||||||
|
<div className="w-16 h-16">
|
||||||
|
<Spinner size="large" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,7 +1,19 @@
|
|||||||
import styles from './styles.module.scss'
|
import styles from './styles.module.scss'
|
||||||
|
import { cn } from '@/shared/utils'
|
||||||
|
|
||||||
export function Spinner() {
|
interface SpinnerProps {
|
||||||
|
size?: 'small' | 'large'
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Spinner({ size = 'small', className }: SpinnerProps) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.spinner} />
|
<div
|
||||||
|
className={cn(
|
||||||
|
styles.spinner,
|
||||||
|
size === 'large' && styles.spinnerLarge,
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,16 @@
|
|||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.spinnerLarge {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
border-width: 4px;
|
||||||
|
margin-right: 0;
|
||||||
|
border-color: hsl(var(--foreground) / 0.3);
|
||||||
|
border-left-color: hsl(var(--foreground));
|
||||||
|
border-top-color: hsl(var(--foreground));
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
0% { transform: rotate(0deg); }
|
0% { transform: rotate(0deg); }
|
||||||
100% { transform: rotate(360deg); }
|
100% { transform: rotate(360deg); }
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ function NavBarItem({ url, children }: React.PropsWithChildren<{
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li>
|
<>
|
||||||
{isLoading && isLoading === url ? (
|
{isLoading && isLoading === url ? (
|
||||||
button
|
button
|
||||||
) : (
|
) : (
|
||||||
@@ -124,6 +124,6 @@ function NavBarItem({ url, children }: React.PropsWithChildren<{
|
|||||||
{button}
|
{button}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</li>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import React from 'react'
|
||||||
import type { Day as DayType } from '@/shared/model/day'
|
import type { Day as DayType } from '@/shared/model/day'
|
||||||
import { getDayOfWeek } from '@/shared/utils'
|
import { getDayOfWeek } from '@/shared/utils'
|
||||||
import { Lesson } from '@/widgets/schedule/lesson'
|
import { Lesson } from '@/widgets/schedule/lesson'
|
||||||
@@ -48,9 +49,10 @@ export function Day({ day }: {
|
|||||||
<div className='snap-start hidden md:block' style={{ flex: '0 0 3rem' }} />
|
<div className='snap-start hidden md:block' style={{ flex: '0 0 3rem' }} />
|
||||||
{day.lessons.map((lesson, i) => (
|
{day.lessons.map((lesson, i) => (
|
||||||
<Lesson
|
<Lesson
|
||||||
|
key={i}
|
||||||
width={longNames ? 450 : 350}
|
width={longNames ? 450 : 350}
|
||||||
lesson={lesson}
|
lesson={lesson}
|
||||||
key={i}
|
animationDelay={i * 0.08}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<div className='snap-start hidden md:block' style={{ flex: `0 0 calc(100vw - 4rem - ${longNames ? 450 : 350}px - 1rem)` }} />
|
<div className='snap-start hidden md:block' style={{ flex: `0 0 calc(100vw - 4rem - ${longNames ? 450 : 350}px - 1rem)` }} />
|
||||||
|
|||||||
@@ -50,7 +50,15 @@ export function Schedule({ days }: {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col p-4 md:p-8 lg:p-16 gap-6 md:gap-12 lg:gap-14">
|
<div className="flex flex-col p-4 md:p-8 lg:p-16 gap-6 md:gap-12 lg:gap-14">
|
||||||
{days.map((day, i) => (
|
{days.map((day, i) => (
|
||||||
<Day day={day} key={`${group}_day${i}`} />
|
<div
|
||||||
|
key={`${group}_day${i}`}
|
||||||
|
className="stagger-card"
|
||||||
|
style={{
|
||||||
|
animationDelay: `${i * 0.1}s`,
|
||||||
|
} as React.CSSProperties}
|
||||||
|
>
|
||||||
|
<Day day={day} />
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -22,9 +22,10 @@ import { BsFillGeoAltFill } from 'react-icons/bs'
|
|||||||
import { RiGroup2Fill } from 'react-icons/ri'
|
import { RiGroup2Fill } from 'react-icons/ri'
|
||||||
import { ResourcesDialog } from '@/widgets/schedule/resources-dialog'
|
import { ResourcesDialog } from '@/widgets/schedule/resources-dialog'
|
||||||
|
|
||||||
export function Lesson({ lesson, width = 350 }: {
|
export function Lesson({ lesson, width = 350, animationDelay }: {
|
||||||
lesson: LessonType
|
lesson: LessonType
|
||||||
width: number
|
width: number
|
||||||
|
animationDelay?: number
|
||||||
}) {
|
}) {
|
||||||
const [resourcesDialogOpened, setResourcesDialogOpened] = React.useState(false)
|
const [resourcesDialogOpened, setResourcesDialogOpened] = React.useState(false)
|
||||||
|
|
||||||
@@ -63,7 +64,12 @@ export function Lesson({ lesson, width = 350 }: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className={`w-full ${width === 450 ? 'md:w-[450px] md:min-w-[450px] md:max-w-[450px]' : 'md:w-[350px] md:min-w-[350px] md:max-w-[350px]'} flex flex-col relative overflow-hidden snap-start scroll-ml-16 shrink-0`}>
|
<Card
|
||||||
|
className={`w-full ${width === 450 ? 'md:w-[450px] md:min-w-[450px] md:max-w-[450px]' : 'md:w-[350px] md:min-w-[350px] md:max-w-[350px]'} flex flex-col relative overflow-hidden snap-start scroll-ml-16 shrink-0 stagger-card`}
|
||||||
|
style={animationDelay !== undefined ? {
|
||||||
|
animationDelay: `${animationDelay}s`,
|
||||||
|
} as React.CSSProperties : undefined}
|
||||||
|
>
|
||||||
{lesson.isChange && <div className='absolute top-0 left-0 w-full h-full bg-gradient-to-br from-[#ffc60026] to-[#95620026] pointer-events-none'></div>}
|
{lesson.isChange && <div className='absolute top-0 left-0 w-full h-full bg-gradient-to-br from-[#ffc60026] to-[#95620026] pointer-events-none'></div>}
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className='flex gap-2 md:gap-4'>
|
<div className='flex gap-2 md:gap-4'>
|
||||||
|
|||||||
Reference in New Issue
Block a user