refactor: add shared club detail layout with persistent tab navigation
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
- Create ClubTabNavigation component with responsive horizontal scroll - Create ClubDetailHeader component with shared layout (breadcrumb, header, tabs) - Add layout.tsx for club_id route to wrap all sub-pages - Extract Courts tab into separate page route - Remove redundant AdminAuthGuard from individual pages (now in layout) - Remove container wrappers from components (now in shared header) - Remove "Back to club" links (breadcrumb now in header) - Standardize colors: gray-* to slate-* for consistent dark text stylingmaster
parent
308d9d70bf
commit
256eccc997
@ -1,252 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { ArrowLeft, Loader2, AlertCircle, Lock } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { getAdminClubDetail } from '@/src/lib/api/admin-clubs';
|
||||
import type { AdminClubDetail, AdminApiError } from '@/src/types/admin-api';
|
||||
import useTranslation from '@/src/hooks/useTranslation';
|
||||
import ClubProfileTab from './tabs/ClubProfileTab';
|
||||
import ClubCourtsTab from './tabs/ClubCourtsTab';
|
||||
|
||||
interface ClubDetailTabsProps {
|
||||
clubId: number;
|
||||
}
|
||||
|
||||
type TabKey = 'profile' | 'courts' | 'slot-definitions';
|
||||
|
||||
export default function ClubDetailTabs({ clubId }: ClubDetailTabsProps) {
|
||||
const { t, locale } = useTranslation();
|
||||
const [clubDetail, setClubDetail] = useState<AdminClubDetail | null>(null);
|
||||
const [error, setError] = useState<AdminApiError | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activeTab, setActiveTab] = useState<TabKey>('profile');
|
||||
|
||||
useEffect(() => {
|
||||
loadClubDetail();
|
||||
}, [clubId]);
|
||||
|
||||
async function loadClubDetail() {
|
||||
setLoading(true);
|
||||
const result = await getAdminClubDetail(clubId);
|
||||
|
||||
if (result.success) {
|
||||
setClubDetail(result.data);
|
||||
setError(null);
|
||||
} else {
|
||||
setError(result.error);
|
||||
setClubDetail(null);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
// Loading state
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20">
|
||||
<div className="flex flex-col items-center justify-center space-y-4">
|
||||
<Loader2 className="w-12 h-12 text-slate-900 animate-spin" />
|
||||
<p className="text-slate-600 font-medium">Loading club details...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Authentication error (401)
|
||||
if (error && error.status === 401) {
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20">
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<div className="bg-amber-50 border-2 border-amber-200 rounded-2xl p-8">
|
||||
<div className="flex items-start space-x-4">
|
||||
<Lock className="w-8 h-8 text-amber-700 flex-shrink-0 mt-1" />
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-slate-900 mb-4">
|
||||
Authentication Required
|
||||
</h2>
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
Please log in to access the venue management portal.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Forbidden error (403)
|
||||
if (error && error.status === 403) {
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20">
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<div className="bg-red-50 border-2 border-red-200 rounded-2xl p-8">
|
||||
<div className="flex items-start space-x-4">
|
||||
<Lock className="w-8 h-8 text-red-600 flex-shrink-0 mt-1" />
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-slate-900 mb-4">
|
||||
Access Denied
|
||||
</h2>
|
||||
<p className="text-slate-700 mb-6 leading-relaxed">
|
||||
{error.detail}
|
||||
</p>
|
||||
<Link
|
||||
href={`/${locale}/admin/clubs`}
|
||||
className="inline-flex items-center px-4 py-2 bg-slate-900 text-white font-semibold rounded-lg hover:bg-slate-800 transition-colors"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
Back to clubs
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Other API errors
|
||||
if (error) {
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20">
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<div className="bg-red-50 border-2 border-red-200 rounded-2xl p-8">
|
||||
<div className="flex items-start space-x-4">
|
||||
<AlertCircle className="w-8 h-8 text-red-600 flex-shrink-0 mt-1" />
|
||||
<div className="flex-1">
|
||||
<h2 className="text-2xl font-bold text-slate-900 mb-4">
|
||||
Error Loading Club
|
||||
</h2>
|
||||
<p className="text-slate-700 mb-4 leading-relaxed">
|
||||
{error.detail}
|
||||
</p>
|
||||
<p className="text-sm text-slate-600 font-mono mb-6">
|
||||
Error code: {error.code}
|
||||
</p>
|
||||
<Link
|
||||
href={`/${locale}/admin/clubs`}
|
||||
className="inline-flex items-center px-4 py-2 bg-slate-900 text-white font-semibold rounded-lg hover:bg-slate-800 transition-colors"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
Back to clubs
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!clubDetail) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Success - render tabbed interface
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
{/* Breadcrumb */}
|
||||
<div className="mb-8">
|
||||
<Link
|
||||
href={`/${locale}/admin/clubs`}
|
||||
className="inline-flex items-center text-slate-600 hover:text-slate-900 font-medium transition-colors"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
Back to clubs
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Club Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-4xl font-bold text-slate-900 mb-2 tracking-tight">
|
||||
{clubDetail.facility.name}
|
||||
</h1>
|
||||
<p className="text-lg text-slate-600 font-light">
|
||||
{clubDetail.facility.timezone}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Tab Navigation */}
|
||||
<div className="border-b-2 border-slate-200 mb-8">
|
||||
<div className="flex space-x-1">
|
||||
<button
|
||||
onClick={() => setActiveTab('profile')}
|
||||
className={`px-6 py-3 font-semibold transition-colors border-b-2 ${
|
||||
activeTab === 'profile'
|
||||
? 'border-slate-900 text-slate-900'
|
||||
: 'border-transparent text-slate-600 hover:text-slate-900 hover:border-slate-300'
|
||||
}`}
|
||||
>
|
||||
Profile
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('courts')}
|
||||
className={`px-6 py-3 font-semibold transition-colors border-b-2 ${
|
||||
activeTab === 'courts'
|
||||
? 'border-slate-900 text-slate-900'
|
||||
: 'border-transparent text-slate-600 hover:text-slate-900 hover:border-slate-300'
|
||||
}`}
|
||||
>
|
||||
Courts
|
||||
</button>
|
||||
<Link
|
||||
href={`/${locale}/admin/clubs/${clubId}/slot-definitions`}
|
||||
className="px-6 py-3 font-semibold text-slate-600 hover:text-slate-900 hover:border-slate-300 transition-colors border-b-2 border-transparent"
|
||||
>
|
||||
Slot Definitions
|
||||
</Link>
|
||||
<Link
|
||||
href={`/${locale}/admin/clubs/${clubId}/slot-instances`}
|
||||
className="px-6 py-3 font-semibold text-slate-600 hover:text-slate-900 hover:border-slate-300 transition-colors border-b-2 border-transparent"
|
||||
>
|
||||
Slot Instances
|
||||
</Link>
|
||||
<Link
|
||||
href={`/${locale}/admin/clubs/${clubId}/plans`}
|
||||
className="px-6 py-3 font-semibold text-purple-600 hover:text-purple-700 hover:border-purple-300 transition-colors border-b-2 border-transparent"
|
||||
>
|
||||
Plans
|
||||
</Link>
|
||||
<Link
|
||||
href={`/${locale}/admin/clubs/${clubId}/members`}
|
||||
className="px-6 py-3 font-semibold text-purple-600 hover:text-purple-700 hover:border-purple-300 transition-colors border-b-2 border-transparent"
|
||||
>
|
||||
Members
|
||||
</Link>
|
||||
<Link
|
||||
href={`/${locale}/admin/clubs/${clubId}/settings`}
|
||||
className="px-6 py-3 font-semibold text-purple-600 hover:text-purple-700 hover:border-purple-300 transition-colors border-b-2 border-transparent"
|
||||
>
|
||||
Settings
|
||||
</Link>
|
||||
<Link
|
||||
href={`/${locale}/admin/clubs/${clubId}/credits`}
|
||||
className="px-6 py-3 font-semibold text-emerald-600 hover:text-emerald-700 hover:border-emerald-300 transition-colors border-b-2 border-transparent"
|
||||
>
|
||||
Credits
|
||||
</Link>
|
||||
<Link
|
||||
href={`/${locale}/admin/clubs/${clubId}/transfers`}
|
||||
className="px-6 py-3 font-semibold text-amber-600 hover:text-amber-700 hover:border-amber-300 transition-colors border-b-2 border-transparent"
|
||||
>
|
||||
Transfers
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tab Content */}
|
||||
{activeTab === 'profile' && (
|
||||
<ClubProfileTab clubId={clubId} onUpdate={loadClubDetail} />
|
||||
)}
|
||||
|
||||
{activeTab === 'courts' && (
|
||||
<ClubCourtsTab
|
||||
clubId={clubId}
|
||||
courts={clubDetail.courts}
|
||||
onUpdate={loadClubDetail}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
return <ClubProfileTab clubId={clubId} />;
|
||||
}
|
||||
|
||||
@ -0,0 +1,47 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { getAdminClubDetail } from '@/src/lib/api/admin-clubs';
|
||||
import type { Court } from '@/src/types/courts';
|
||||
import ClubCourtsTab from '../tabs/ClubCourtsTab';
|
||||
|
||||
interface CourtsComponentProps {
|
||||
clubId: number;
|
||||
}
|
||||
|
||||
export default function CourtsComponent({ clubId }: CourtsComponentProps) {
|
||||
const [courts, setCourts] = useState<Court[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
loadCourts();
|
||||
}, [clubId]);
|
||||
|
||||
async function loadCourts() {
|
||||
setLoading(true);
|
||||
const result = await getAdminClubDetail(clubId);
|
||||
|
||||
if (result.success) {
|
||||
setCourts(result.data.courts);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<Loader2 className="w-8 h-8 text-slate-600 animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ClubCourtsTab
|
||||
clubId={clubId}
|
||||
courts={courts}
|
||||
onUpdate={loadCourts}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
import CourtsComponent from './CourtsComponent';
|
||||
|
||||
export default async function CourtsPage({
|
||||
params
|
||||
}: {
|
||||
params: Promise<{ club_id: string }>
|
||||
}) {
|
||||
const { club_id } = await params;
|
||||
const clubId = parseInt(club_id, 10);
|
||||
|
||||
return <CourtsComponent clubId={clubId} />;
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
import AdminAuthGuard from '@/src/components/AdminAuthGuard';
|
||||
import ClubDetailHeader from '@/src/components/ClubDetailHeader';
|
||||
|
||||
export default async function ClubDetailLayout({
|
||||
children,
|
||||
params,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
params: Promise<{ club_id: string }>;
|
||||
}) {
|
||||
const { club_id } = await params;
|
||||
const clubId = parseInt(club_id, 10);
|
||||
|
||||
return (
|
||||
<AdminAuthGuard>
|
||||
<ClubDetailHeader clubId={clubId}>
|
||||
{children}
|
||||
</ClubDetailHeader>
|
||||
</AdminAuthGuard>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,173 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { ArrowLeft, Loader2, AlertCircle, Lock } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { getAdminClubDetail } from '@/src/lib/api/admin-clubs';
|
||||
import type { AdminClubDetail, AdminApiError } from '@/src/types/admin-api';
|
||||
import useTranslation from '@/src/hooks/useTranslation';
|
||||
import ClubTabNavigation from './ClubTabNavigation';
|
||||
|
||||
interface ClubDetailHeaderProps {
|
||||
clubId: number;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function ClubDetailHeader({ clubId, children }: ClubDetailHeaderProps) {
|
||||
const { locale } = useTranslation();
|
||||
const [clubDetail, setClubDetail] = useState<AdminClubDetail | null>(null);
|
||||
const [error, setError] = useState<AdminApiError | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
loadClubDetail();
|
||||
}, [clubId]);
|
||||
|
||||
async function loadClubDetail() {
|
||||
setLoading(true);
|
||||
const result = await getAdminClubDetail(clubId);
|
||||
|
||||
if (result.success) {
|
||||
setClubDetail(result.data);
|
||||
setError(null);
|
||||
} else {
|
||||
setError(result.error);
|
||||
setClubDetail(null);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
// Loading state
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20">
|
||||
<div className="flex flex-col items-center justify-center space-y-4">
|
||||
<Loader2 className="w-12 h-12 text-slate-900 animate-spin" />
|
||||
<p className="text-slate-600 font-medium">Loading club details...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Authentication error (401)
|
||||
if (error && error.status === 401) {
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20">
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<div className="bg-amber-50 border-2 border-amber-200 rounded-2xl p-8">
|
||||
<div className="flex items-start space-x-4">
|
||||
<Lock className="w-8 h-8 text-amber-700 flex-shrink-0 mt-1" />
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-slate-900 mb-4">
|
||||
Authentication Required
|
||||
</h2>
|
||||
<p className="text-slate-700 leading-relaxed">
|
||||
Please log in to access the venue management portal.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Forbidden error (403)
|
||||
if (error && error.status === 403) {
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20">
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<div className="bg-red-50 border-2 border-red-200 rounded-2xl p-8">
|
||||
<div className="flex items-start space-x-4">
|
||||
<Lock className="w-8 h-8 text-red-600 flex-shrink-0 mt-1" />
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-slate-900 mb-4">
|
||||
Access Denied
|
||||
</h2>
|
||||
<p className="text-slate-700 mb-6 leading-relaxed">
|
||||
{error.detail}
|
||||
</p>
|
||||
<Link
|
||||
href={`/${locale}/admin/clubs`}
|
||||
className="inline-flex items-center px-4 py-2 bg-slate-900 text-white font-semibold rounded-lg hover:bg-slate-800 transition-colors"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
Back to clubs
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Other API errors
|
||||
if (error) {
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20">
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<div className="bg-red-50 border-2 border-red-200 rounded-2xl p-8">
|
||||
<div className="flex items-start space-x-4">
|
||||
<AlertCircle className="w-8 h-8 text-red-600 flex-shrink-0 mt-1" />
|
||||
<div className="flex-1">
|
||||
<h2 className="text-2xl font-bold text-slate-900 mb-4">
|
||||
Error Loading Club
|
||||
</h2>
|
||||
<p className="text-slate-700 mb-4 leading-relaxed">
|
||||
{error.detail}
|
||||
</p>
|
||||
<p className="text-sm text-slate-600 font-mono mb-6">
|
||||
Error code: {error.code}
|
||||
</p>
|
||||
<Link
|
||||
href={`/${locale}/admin/clubs`}
|
||||
className="inline-flex items-center px-4 py-2 bg-slate-900 text-white font-semibold rounded-lg hover:bg-slate-800 transition-colors"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
Back to clubs
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!clubDetail) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
{/* Breadcrumb */}
|
||||
<div className="mb-8">
|
||||
<Link
|
||||
href={`/${locale}/admin/clubs`}
|
||||
className="inline-flex items-center text-slate-600 hover:text-slate-900 font-medium transition-colors"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
Back to clubs
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Club Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-4xl font-bold text-slate-900 mb-2 tracking-tight">
|
||||
{clubDetail.facility.name}
|
||||
</h1>
|
||||
<p className="text-lg text-slate-600 font-light">
|
||||
{clubDetail.facility.timezone}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Tab Navigation */}
|
||||
<ClubTabNavigation clubId={clubId} />
|
||||
|
||||
{/* Content */}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,69 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import useTranslation from '@/src/hooks/useTranslation';
|
||||
|
||||
interface ClubTabNavigationProps {
|
||||
clubId: number;
|
||||
}
|
||||
|
||||
interface TabConfig {
|
||||
key: string;
|
||||
label: string;
|
||||
href: string;
|
||||
}
|
||||
|
||||
export default function ClubTabNavigation({ clubId }: ClubTabNavigationProps) {
|
||||
const { locale } = useTranslation();
|
||||
const pathname = usePathname();
|
||||
|
||||
const basePath = `/${locale}/admin/clubs/${clubId}`;
|
||||
|
||||
const tabs: TabConfig[] = [
|
||||
{ key: 'profile', label: 'Profile', href: basePath },
|
||||
{ key: 'courts', label: 'Courts', href: `${basePath}/courts` },
|
||||
{ key: 'slot-definitions', label: 'Slot Definitions', href: `${basePath}/slot-definitions` },
|
||||
{ key: 'slot-instances', label: 'Slot Instances', href: `${basePath}/slot-instances` },
|
||||
{ key: 'plans', label: 'Plans', href: `${basePath}/plans` },
|
||||
{ key: 'members', label: 'Members', href: `${basePath}/members` },
|
||||
{ key: 'settings', label: 'Settings', href: `${basePath}/settings` },
|
||||
{ key: 'credits', label: 'Credits', href: `${basePath}/credits` },
|
||||
{ key: 'transfers', label: 'Transfers', href: `${basePath}/transfers` },
|
||||
];
|
||||
|
||||
function isActive(tab: TabConfig): boolean {
|
||||
// For the base club path (profile), check if we're exactly on it
|
||||
if (tab.key === 'profile') {
|
||||
// Active if pathname is exactly the basePath (no sub-routes)
|
||||
return pathname === basePath;
|
||||
}
|
||||
// For sub-pages, check if pathname starts with the tab's href
|
||||
return pathname.startsWith(tab.href);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border-b-2 border-slate-200 mb-8">
|
||||
<div className="overflow-x-auto scrollbar-hide -mb-px">
|
||||
<div className="flex min-w-max">
|
||||
{tabs.map((tab) => {
|
||||
const active = isActive(tab);
|
||||
return (
|
||||
<Link
|
||||
key={tab.key}
|
||||
href={tab.href}
|
||||
className={`px-4 sm:px-6 py-3 font-semibold transition-colors border-b-2 whitespace-nowrap text-sm sm:text-base ${
|
||||
active
|
||||
? 'border-slate-900 text-slate-900'
|
||||
: 'border-transparent text-slate-600 hover:text-slate-900 hover:border-slate-300'
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue