Refactor PlatformLineChart and Tabs components for improved layout and styling, enhance WhitelistSettings with responsive design, and implement API candles health check in HealthChecks. Update global styles for scrollbar visibility and adjust tool tabs for better organization.

This commit is contained in:
2025-11-08 04:56:41 +07:00
parent 83d13bde74
commit 044ffcc6f5
10 changed files with 285 additions and 221 deletions

View File

@@ -265,13 +265,18 @@ function PlatformLineChart({
color: theme['base-content'] || '#ffffff'
},
bgcolor: 'rgba(0,0,0,0)',
bordercolor: 'rgba(0,0,0,0)'
bordercolor: 'rgba(0,0,0,0)',
orientation: 'h' as const,
x: 0.5,
y: -0.15,
xanchor: 'center' as const,
yanchor: 'top' as const
},
margin: {
l: 60,
r: 60,
t: 60,
b: 60,
b: 80,
pad: 4
},
paper_bgcolor: 'rgba(0,0,0,0)',
@@ -290,9 +295,8 @@ function PlatformLineChart({
}
const config: Partial<Config> = {
displayModeBar: true,
displayModeBar: false,
displaylogo: false,
modeBarButtonsToRemove: ['pan2d', 'lasso2d', 'select2d', 'autoScale2d', 'resetScale2d'] as any,
responsive: true
}

View File

@@ -27,40 +27,42 @@ const Tabs: FC<ITabsProps> = ({
orientation === 'vertical' ? className + ' vertical' : className
}
>
<div className="tabs" role="tablist" aria-orientation={orientation}>
{tabs.map((tab: any) => (
<button
className={
'mb-5 tab tab-bordered ' +
(selectedTab === tab.index ? 'tab-active' : '')
}
onClick={() => onClick(tab.index)}
key={tab.index}
type="button"
role="tab"
aria-selected={selectedTab === tab.index}
aria-controls={`tabpanel-${tab.index}`}
tabIndex={selectedTab === tab.index ? 0 : -1}
id={`btn-${tab.index}`}
>
{tab.label}
</button>
))}
{addButton && (
<button
className="tab tab-bordered mb-5"
onClick={onAddButton}
key={'add'}
type="button"
role="tab"
aria-selected={false}
aria-controls={`tabpanel-${'add'}`}
tabIndex={-1}
id={`btn-${'add'}`}
>
+
</button>
)}
<div className="overflow-x-auto scrollbar-hide -mx-4 px-4">
<div className="tabs flex-nowrap min-w-max" role="tablist" aria-orientation={orientation}>
{tabs.map((tab: any) => (
<button
className={
'mb-5 tab tab-bordered whitespace-nowrap flex-shrink-0 ' +
(selectedTab === tab.index ? 'tab-active' : '')
}
onClick={() => onClick(tab.index)}
key={tab.index}
type="button"
role="tab"
aria-selected={selectedTab === tab.index}
aria-controls={`tabpanel-${tab.index}`}
tabIndex={selectedTab === tab.index ? 0 : -1}
id={`btn-${tab.index}`}
>
{tab.label}
</button>
))}
{addButton && (
<button
className="tab tab-bordered mb-5 whitespace-nowrap flex-shrink-0"
onClick={onAddButton}
key={'add'}
type="button"
role="tab"
aria-selected={false}
aria-controls={`tabpanel-${'add'}`}
tabIndex={-1}
id={`btn-${'add'}`}
>
+
</button>
)}
</div>
</div>
<div
role="tabpanel"

View File

@@ -72,27 +72,27 @@ const WhitelistSettings: React.FC = () => {
Search Filters
</div>
<div className="collapse-content">
<div className="flex gap-4 pt-4">
<div className="form-control w-full max-w-xs">
<label className="label">
<span className="label-text">Search by Ethereum Account</span>
<div className="flex gap-2 pt-2">
<div className="form-control w-full max-w-[200px]">
<label className="label py-1">
<span className="label-text text-xs">Search by Ethereum Account</span>
</label>
<input
type="text"
placeholder="Enter Ethereum address..."
className="input input-bordered w-full"
className="input input-bordered input-sm w-full"
value={searchExternalEthereumAccount}
onChange={(e) => handleSearchExternalEthereum(e.target.value)}
/>
</div>
<div className="form-control w-full max-w-xs">
<label className="label">
<span className="label-text">Search by Twitter Account</span>
<div className="form-control w-full max-w-[200px]">
<label className="label py-1">
<span className="label-text text-xs">Search by Twitter Account</span>
</label>
<input
type="text"
placeholder="Enter Twitter handle..."
className="input input-bordered w-full"
className="input input-bordered input-sm w-full"
value={searchTwitterAccount}
onChange={(e) => handleSearchTwitter(e.target.value)}
/>

View File

@@ -3,8 +3,7 @@ import BundleRequestsTable from './bundleRequestsTable';
const BacktestBundleForm: React.FC = () => {
return (
<div className="p-10 max-w-7xl mx-auto">
<h2 className="text-2xl font-bold mb-6">Bundle Backtest</h2>
<div className="p-4 md:p-6 lg:p-8 w-full">
<BundleRequestsTable />
</div>
);

View File

@@ -135,7 +135,7 @@ function PlatformSummary({index}: { index: number }) {
<div className="grid grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
{/* Total Volume Traded */}
<div className="card bg-base-200">
<div className="card-body">
<div className="card-body p-3 md:p-6">
<h3 className="card-title text-base-content/70">Total Volume Traded</h3>
<div className="text-3xl font-bold text-base-content mb-1">
{formatCurrency(platformData?.totalPlatformVolume || 0)}
@@ -189,36 +189,36 @@ function PlatformSummary({index}: { index: number }) {
{/* Top 3 Most Profitable */}
<div className="card bg-base-200">
<div className="card-body">
<div className="flex items-center gap-2 mb-4">
<span className="text-2xl">🤑</span>
<h3 className="card-title text-base-content/70">Top 3 Most Profitable</h3>
<div className="card-body p-3 md:p-6">
<div className="flex items-center gap-2 mb-3 md:mb-4">
<span className="text-xl md:text-2xl">🤑</span>
<h3 className="card-title text-base-content/70 text-sm md:text-base">Top 3 Most Profitable</h3>
</div>
<div className="space-y-3">
<div className="space-y-2 md:space-y-3">
{topStrategies?.topStrategies?.slice(0, 3).map((strategy, index) => (
<div key={index} className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="avatar placeholder">
<div className="bg-primary text-primary-content rounded-full w-8">
<div key={index} className="flex items-center justify-between gap-1 md:gap-2">
<div className="flex items-center gap-1.5 md:gap-3 min-w-0 flex-1">
<div className="avatar placeholder flex-shrink-0 hidden md:flex">
<div className="bg-primary text-primary-content rounded-full w-8 h-8">
<span className="text-xs font-bold">
{strategy.strategyName?.charAt(0) || 'S'}
</span>
</div>
</div>
<div>
<div className="text-sm text-base-content font-medium">
<div className="min-w-0 flex-1">
<div className="text-xs md:text-sm text-base-content font-medium truncate">
{strategy.strategyName || '[Strategy Name]'}
</div>
<div className="text-xs text-base-content/60">📧</div>
<div className="text-[10px] md:text-xs text-base-content/60">📧</div>
</div>
</div>
<div
className={`text-sm font-bold ${strategy.netPnL && strategy.netPnL >= 0 ? 'text-success' : 'text-error'}`}>
className={`text-xs md:text-sm font-bold flex-shrink-0 ${strategy.netPnL && strategy.netPnL >= 0 ? 'text-success' : 'text-error'}`}>
{strategy.netPnL && strategy.netPnL >= 0 ? '+' : ''}{formatCurrency(strategy.netPnL || 0)}
</div>
</div>
)) || (
<div className="text-base-content/60 text-sm">No profitable strategies found</div>
<div className="text-base-content/60 text-xs md:text-sm">No profitable strategies found</div>
)}
</div>
</div>
@@ -226,44 +226,44 @@ function PlatformSummary({index}: { index: number }) {
{/* Top 3 Rising (by ROI) */}
<div className="card bg-base-200">
<div className="card-body">
<div className="flex items-center gap-2 mb-4">
<span className="text-2xl">📈</span>
<h3 className="card-title text-base-content/70">Top 3 by ROI</h3>
<div className="card-body p-3 md:p-6">
<div className="flex items-center gap-2 mb-3 md:mb-4">
<span className="text-xl md:text-2xl">📈</span>
<h3 className="card-title text-base-content/70 text-sm md:text-base">Top 3 by ROI</h3>
</div>
<div className="space-y-3">
<div className="space-y-2 md:space-y-3">
{topStrategiesByRoi?.topStrategiesByRoi?.slice(0, 3).map((strategy, index) => (
<div key={index} className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="avatar placeholder">
<div className="bg-success text-success-content rounded-full w-8">
<div key={index} className="flex items-center justify-between gap-1 md:gap-2">
<div className="flex items-center gap-1.5 md:gap-3 min-w-0 flex-1">
<div className="avatar placeholder flex-shrink-0 hidden md:flex">
<div className="bg-success text-success-content rounded-full w-8 h-8">
<span className="text-xs font-bold">
{strategy.strategyName?.charAt(0) || 'S'}
</span>
</div>
</div>
<div>
<div className="text-sm text-base-content font-medium">
<div className="min-w-0 flex-1">
<div className="text-xs md:text-sm text-base-content font-medium truncate">
{strategy.strategyName || '[Strategy Name]'}
</div>
<div className="text-xs text-base-content/60">
<div className="text-[10px] md:text-xs text-base-content/60 truncate">
Vol: {formatCurrency(strategy.volume || 0)}
</div>
</div>
</div>
<div className="text-right">
<div className="text-right flex-shrink-0">
<div
className={`text-sm font-bold ${(strategy.roi || 0) >= 0 ? 'text-success' : 'text-error'}`}>
className={`text-xs md:text-sm font-bold ${(strategy.roi || 0) >= 0 ? 'text-success' : 'text-error'}`}>
{(strategy.roi || 0) >= 0 ? '+' : ''}{strategy.roi?.toFixed(2) || 0}%
</div>
<div
className={`text-xs ${(strategy.netPnL || 0) >= 0 ? 'text-success' : 'text-error'}`}>
className={`text-[10px] md:text-xs ${(strategy.netPnL || 0) >= 0 ? 'text-success' : 'text-error'}`}>
{(strategy.netPnL || 0) >= 0 ? '+' : ''}{formatCurrency(strategy.netPnL || 0)}
</div>
</div>
</div>
)) || (
<div className="text-base-content/60 text-sm">No ROI data available</div>
<div className="text-base-content/60 text-xs md:text-sm">No ROI data available</div>
)}
</div>
</div>
@@ -271,43 +271,43 @@ function PlatformSummary({index}: { index: number }) {
{/* Top 3 Agents by PnL */}
<div className="card bg-base-200">
<div className="card-body">
<div className="flex items-center gap-2 mb-4">
<span className="text-2xl">👥</span>
<h3 className="card-title text-base-content/70">Top 3 Agents by PnL</h3>
<div className="card-body p-3 md:p-6">
<div className="flex items-center gap-2 mb-3 md:mb-4">
<span className="text-xl md:text-2xl">👥</span>
<h3 className="card-title text-base-content/70 text-sm md:text-base">Top 3 Agents by PnL</h3>
</div>
<div className="space-y-3">
<div className="space-y-2 md:space-y-3">
{topAgentsByPnL?.slice(0, 3).map((agent, index) => (
<div key={index} className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="avatar placeholder">
<div className="bg-secondary text-secondary-content rounded-full w-8">
<div key={index} className="flex items-center justify-between gap-1 md:gap-2">
<div className="flex items-center gap-1.5 md:gap-3 min-w-0 flex-1">
<div className="avatar placeholder flex-shrink-0 hidden md:flex">
<div className="bg-secondary text-secondary-content rounded-full w-8 h-8">
<span className="text-xs font-bold">
{agent.agentName?.charAt(0) || 'A'}
</span>
</div>
</div>
<div>
<div className="text-sm text-base-content font-medium">
<div className="min-w-0 flex-1">
<div className="text-xs md:text-sm text-base-content font-medium truncate">
{agent.agentName || '[Agent Name]'}
</div>
<div className="text-xs text-base-content/60">
<div className="text-[10px] md:text-xs text-base-content/60 truncate">
{agent.activeStrategiesCount || 0} strategies
</div>
</div>
</div>
<div className="text-right">
<div className="text-right flex-shrink-0">
<div
className={`text-sm font-bold ${(agent.netPnL || 0) >= 0 ? 'text-success' : 'text-error'}`}>
className={`text-xs md:text-sm font-bold ${(agent.netPnL || 0) >= 0 ? 'text-success' : 'text-error'}`}>
{(agent.netPnL || 0) >= 0 ? '+' : ''}{formatCurrency(agent.netPnL || 0)}
</div>
<div className="text-xs text-base-content/60">
<div className="text-[10px] md:text-xs text-base-content/60">
{(agent.totalROI || 0).toFixed(2)}% ROI
</div>
</div>
</div>
)) || (
<div className="text-base-content/60 text-sm">No agent data available</div>
<div className="text-base-content/60 text-xs md:text-sm">No agent data available</div>
)}
</div>
</div>

View File

@@ -1,7 +1,6 @@
import React from 'react'
import {useQuery} from '@tanstack/react-query'
import useApiUrlStore from '../../../app/store/apiStore'
import {Table} from '../../../components/mollecules'
// Define health check response interface based on the provided example
interface HealthCheckEntry {
@@ -48,6 +47,16 @@ const HealthChecks: React.FC = () => {
}
})
// Use TanStack Query for API candles health check
const {data: candlesHealth, isLoading: isLoadingCandles} = useQuery({
queryKey: ['health', 'api-candles'],
queryFn: async () => {
const response = await fetch(`${apiUrl}/health-candles`)
if (!response.ok) throw new Error('Failed to fetch API candles health')
return response.json() as Promise<HealthCheckResponse>
}
})
// Use TanStack Query for Worker health check
const {data: workerHealth, isLoading: isLoadingWorker} = useQuery({
queryKey: ['health', 'worker'],
@@ -59,9 +68,6 @@ const HealthChecks: React.FC = () => {
})
// Determine overall loading state
const isLoading = isLoadingApi || isLoadingWorker
// Helper function to prepare table data from health response
const prepareHealthData = (
service: string,
@@ -85,34 +91,22 @@ const HealthChecks: React.FC = () => {
const results: any[] = [];
Object.entries(health.entries).forEach(([key, entry]) => {
// Basic health check entry
const baseEntry = {
service,
component: key,
status: entry.status,
duration: entry.duration,
tags: entry.tags.join(', '),
description: entry.description || '',
details: null,
};
// Add the base entry
results.push(baseEntry);
// Special handling for candle-data to expand timeframe checks
if (key === 'candle-data' && entry.data) {
// Extract timeframe checks
// Extract timeframe checks - show all ticker-timeframe combinations
Object.entries(entry.data)
.filter(([dataKey]) => dataKey.startsWith('TimeframeCheck_'))
.forEach(([dataKey, timeframeData]) => {
const tfData = timeframeData as CandleTimeframeCheck;
// Create a more descriptive component name with ticker and timeframe
const componentName = `${tfData.CheckedTicker || 'N/A'} - ${tfData.CheckedTimeframe || 'N/A'}`
results.push({
service,
component: `${key} - ${tfData.CheckedTimeframe}`,
component: componentName,
status: tfData.Status,
duration: '',
tags: 'candles',
description: tfData.Message,
description: tfData.Message || '',
details: {
Ticker: tfData.CheckedTicker,
LatestCandle: tfData.LatestCandleDate,
@@ -120,6 +114,47 @@ const HealthChecks: React.FC = () => {
},
});
});
// Also check for ticker results if they exist (from detailed health check)
Object.entries(entry.data)
.filter(([dataKey]) => dataKey.startsWith('TickerResults_') || dataKey === 'TickerResults')
.forEach(([dataKey, tickerData]) => {
if (tickerData && typeof tickerData === 'object') {
Object.entries(tickerData as Record<string, any>).forEach(([ticker, tickerInfo]) => {
if (tickerInfo && typeof tickerInfo === 'object' && Array.isArray((tickerInfo as any).timeframeChecks)) {
const timeframeChecks = (tickerInfo as any).timeframeChecks as CandleTimeframeCheck[];
timeframeChecks.forEach((tfData: CandleTimeframeCheck) => {
const componentName = `${ticker} - ${tfData.CheckedTimeframe || 'N/A'}`
results.push({
service,
component: componentName,
status: tfData.Status,
duration: '',
tags: 'candles',
description: tfData.Message || '',
details: {
Ticker: tfData.CheckedTicker || ticker,
LatestCandle: tfData.LatestCandleDate,
TimeDifference: tfData.TimeDifference,
},
});
});
}
});
}
});
} else {
// Basic health check entry (skip candle-data as it's expanded above)
const baseEntry = {
service,
component: key,
status: entry.status,
duration: entry.duration,
tags: entry.tags.join(', '),
description: entry.description || '',
details: null,
};
results.push(baseEntry);
}
// Special handling for Web3Proxy components
@@ -206,96 +241,112 @@ const HealthChecks: React.FC = () => {
return results;
}
// Combine all health check data for display
const healthData = [
...prepareHealthData('Managing API', apiHealth || null),
...prepareHealthData('Managing Worker', workerHealth || null),
]
// Define columns for the table
const columns = React.useMemo(
() => [
{
Header: 'Service',
accessor: 'service',
disableSortBy: true,
disableFilters: true,
},
{
Header: 'Component',
accessor: 'component',
disableSortBy: true,
disableFilters: true,
},
{
Header: 'Status',
accessor: 'status',
Cell: ({value}: { value: string }) => (
<span
className={`badge ${
value === 'Healthy' || value === 'healthy'
? 'badge-success'
: value === 'Unreachable' || value === 'unhealthy'
? 'badge-error'
: 'badge-warning'
}`}
>
{value.charAt(0).toUpperCase() + value.slice(1)}
</span>
),
disableSortBy: true,
disableFilters: true,
},
{
Header: 'Description',
accessor: 'description',
disableSortBy: true,
disableFilters: true,
Cell: ({value, row}: any) => (
<div>
<div>{value}</div>
{row.original.details && (
<div className="text-xs mt-1 opacity-80">
{Object.entries(row.original.details).filter(([_, val]) => val !== undefined).map(
([key, val]) => (
<div key={key}>
<span className="font-semibold">{key}:</span> {String(val)}
</div>
)
)}
</div>
)}
</div>
),
},
{
Header: 'Duration',
accessor: 'duration',
disableSortBy: true,
disableFilters: true,
},
{
Header: 'Tags',
accessor: 'tags',
disableSortBy: true,
disableFilters: true,
},
],
[]
// Prepare health data for each service independently
const apiHealthData = React.useMemo(() =>
prepareHealthData('Managing API', apiHealth || null),
[apiHealth]
)
const candlesHealthData = React.useMemo(() =>
prepareHealthData('Managing API - Candles', candlesHealth || null),
[candlesHealth]
)
const workerHealthData = React.useMemo(() =>
prepareHealthData('Managing Worker', workerHealth || null),
[workerHealth]
)
// Create service entries with loading states
const services = [
{
name: 'Managing API',
data: apiHealthData,
isLoading: isLoadingApi,
},
{
name: 'Managing API - Candles',
data: candlesHealthData,
isLoading: isLoadingCandles,
},
{
name: 'Managing Worker',
data: workerHealthData,
isLoading: isLoadingWorker,
},
]
// Get overall status for a service
const getServiceStatus = (items: Array<{status: string}>) => {
if (items.some(item => item.status.toLowerCase() === 'unhealthy' || item.status.toLowerCase() === 'unreachable')) {
return { status: 'Unhealthy', color: 'badge-error' }
}
if (items.some(item => item.status.toLowerCase() === 'degraded')) {
return { status: 'Degraded', color: 'badge-warning' }
}
return { status: 'Healthy', color: 'badge-success' }
}
const getStatusBadge = (status: string) => {
const statusLower = status.toLowerCase()
if (statusLower === 'healthy') return 'badge-success badge-xs'
if (statusLower === 'unhealthy' || statusLower === 'unreachable') return 'badge-error badge-xs'
return 'badge-warning badge-xs'
}
return (
<div className="container mx-auto">
<h2 className="text-xl font-bold mb-4">System Health Status</h2>
{isLoading ? (
<progress className="progress progress-primary w-56"></progress>
) : (
<Table
columns={columns}
data={healthData}
showPagination={true}
/>
)}
<div className="container mx-auto p-1 md:p-2">
<div className="space-y-1.5">
{services.map((service) => {
if (service.isLoading && service.data.length === 0) {
return (
<div key={service.name} className="card bg-base-200">
<div className="card-body p-1.5 md:p-2">
<div className="flex items-center justify-between mb-0.5">
<h3 className="text-[10px] md:text-xs font-semibold">{service.name}</h3>
<progress className="progress progress-primary w-16 h-0.5"></progress>
</div>
</div>
</div>
)
}
if (service.data.length === 0) {
return null
}
const serviceStatus = getServiceStatus(service.data)
return (
<div key={service.name} className="card bg-base-200">
<div className="card-body p-1.5 md:p-2">
<div className="flex items-center justify-between mb-0.5">
<h3 className="text-[10px] md:text-xs font-semibold">{service.name}</h3>
<div className="flex items-center gap-1">
{service.isLoading && (
<progress className="progress progress-primary w-8 h-0.5"></progress>
)}
<div className={`badge ${serviceStatus.color} badge-xs px-1 py-0 text-[8px]`}>
{serviceStatus.status}
</div>
</div>
</div>
<div className="grid grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-0.5 md:gap-1">
{service.data.map((item, idx) => (
<div key={idx} className="flex items-center justify-between bg-base-100 rounded px-1 py-0.5 text-[10px]">
<div className="flex-1 min-w-0">
<div className="truncate font-medium leading-tight">{item.component}</div>
</div>
<div className={`badge ${getStatusBadge(item.status)} ml-0.5 flex-shrink-0 px-0.5 py-0 text-[8px]`}>
{item.status.charAt(0).toUpperCase()}
</div>
</div>
))}
</div>
</div>
</div>
)
})}
</div>
</div>
)
}

View File

@@ -265,7 +265,6 @@ const SqlMonitoring: React.FC = () => {
<div className="container mx-auto space-y-3 sm:space-y-6 px-2 sm:px-4">
{/* Header */}
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-2 sm:gap-4">
<h2 className="text-xl sm:text-2xl font-bold">SQL Monitoring</h2>
<button
className="btn btn-outline btn-xs sm:btn-sm"
onClick={() => clearTrackingMutation.mutate()}

View File

@@ -72,27 +72,27 @@ const WhitelistSettings: React.FC = () => {
Search Filters
</div>
<div className="collapse-content">
<div className="flex gap-4 pt-4">
<div className="form-control w-full max-w-xs">
<label className="label">
<span className="label-text">Search by Ethereum Account</span>
<div className="flex gap-2 pt-2">
<div className="form-control w-full max-w-[200px]">
<label className="label py-1">
<span className="label-text text-xs">Search by Ethereum Account</span>
</label>
<input
type="text"
placeholder="Enter Ethereum address..."
className="input input-bordered w-full"
className="input input-bordered input-sm w-full"
value={searchExternalEthereumAccount}
onChange={(e) => handleSearchExternalEthereum(e.target.value)}
/>
</div>
<div className="form-control w-full max-w-xs">
<label className="label">
<span className="label-text">Search by Twitter Account</span>
<div className="form-control w-full max-w-[200px]">
<label className="label py-1">
<span className="label-text text-xs">Search by Twitter Account</span>
</label>
<input
type="text"
placeholder="Enter Twitter handle..."
className="input input-bordered w-full"
className="input input-bordered input-sm w-full"
value={searchTwitterAccount}
onChange={(e) => handleSearchTwitter(e.target.value)}
/>

View File

@@ -9,19 +9,19 @@ import FeeCalculator from './feeCalculator'
const tabs: ITabsType = [
{
Component: SpotlightView,
Component: FeeCalculator,
index: 1,
label: 'Fee Calculator',
},
{
Component: SpotlightView,
index: 2,
label: 'Spotlight',
},
{
Component: RektFees,
index: 2,
label: 'RektFees',
},
{
Component: FeeCalculator,
index: 3,
label: 'Fee Calculator',
label: 'RektFees',
},
]

View File

@@ -26,6 +26,15 @@
.layout {
@apply sm:w-11/12 w-10/12 mx-auto;
}
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
}
@layer components {