feat: add dark mode, navigation, and Web Vitals tracking to marketplace
Backend: - Simplify DatabaseConfig: remove effective_url property and project root finder - Update to Pydantic v2 model_config (replace nested Config class) - Add web_vitals router to main.py and __init__.py - Fix ExplorerService datetime handling (ensure timezone-naive comparisons) - Fix status_label extraction to handle both enum and string job states Frontend (Marketplace): - Add dark mode toggle with system preference detection
This commit is contained in:
@@ -1,9 +1,17 @@
|
||||
// Type declarations for global objects
|
||||
declare global {
|
||||
interface Window {
|
||||
analytics?: {
|
||||
track: (event: string, data: any) => void;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
import './style.css';
|
||||
import {
|
||||
fetchMarketplaceOffers,
|
||||
fetchMarketplaceStats,
|
||||
submitMarketplaceBid,
|
||||
MARKETPLACE_CONFIG,
|
||||
} from './lib/api';
|
||||
import type { MarketplaceOffer, MarketplaceStats } from './lib/api';
|
||||
|
||||
@@ -16,9 +24,21 @@ if (!app) {
|
||||
app.innerHTML = `
|
||||
<main>
|
||||
<header class="page-header">
|
||||
<p>Data mode: <strong>${MARKETPLACE_CONFIG.dataMode.toUpperCase()}</strong></p>
|
||||
<h1>Marketplace Control Center</h1>
|
||||
<p>Monitor available offers, submit bids, and review marketplace health at a glance.</p>
|
||||
<nav class="page-header__nav">
|
||||
<a href="/" class="back-link">← Home</a>
|
||||
<a href="/explorer/">Explorer</a>
|
||||
<a href="/Exchange/">Exchange</a>
|
||||
</nav>
|
||||
<div class="page-header-content">
|
||||
<div class="page-header-title">
|
||||
<h1>Marketplace Control Center</h1>
|
||||
<p>Monitor available offers, submit bids, and review marketplace health at a glance.</p>
|
||||
</div>
|
||||
<button onclick="toggleDarkMode()" class="dark-mode-toggle" title="Toggle dark mode">
|
||||
<span id="darkModeEmoji">🌙</span>
|
||||
<span id="darkModeText">Dark</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="dashboard-grid" id="stats-panel">
|
||||
@@ -80,13 +100,13 @@ app.innerHTML = `
|
||||
`;
|
||||
|
||||
const selectors = {
|
||||
totalOffers: document.querySelector<HTMLSpanElement>('#stat-total-offers'),
|
||||
openCapacity: document.querySelector<HTMLSpanElement>('#stat-open-capacity'),
|
||||
averagePrice: document.querySelector<HTMLSpanElement>('#stat-average-price'),
|
||||
activeBids: document.querySelector<HTMLSpanElement>('#stat-active-bids'),
|
||||
offersWrapper: document.querySelector<HTMLDivElement>('#offers-table-wrapper'),
|
||||
bidForm: document.querySelector<HTMLFormElement>('#bid-form'),
|
||||
toast: document.querySelector<HTMLDivElement>('#toast'),
|
||||
totalOffers: document.querySelector<HTMLSpanElement>('#stat-total-offers')!,
|
||||
openCapacity: document.querySelector<HTMLSpanElement>('#stat-open-capacity')!,
|
||||
averagePrice: document.querySelector<HTMLSpanElement>('#stat-average-price')!,
|
||||
activeBids: document.querySelector<HTMLSpanElement>('#stat-active-bids')!,
|
||||
offersWrapper: document.querySelector<HTMLDivElement>('#offers-table-wrapper')!,
|
||||
bidForm: document.querySelector<HTMLFormElement>('#bid-form')!,
|
||||
toast: document.querySelector<HTMLDivElement>('#toast')!,
|
||||
};
|
||||
|
||||
function formatNumber(value: number, options: Intl.NumberFormatOptions = {}): string {
|
||||
@@ -94,13 +114,13 @@ function formatNumber(value: number, options: Intl.NumberFormatOptions = {}): st
|
||||
}
|
||||
|
||||
function renderStats(stats: MarketplaceStats): void {
|
||||
selectors.totalOffers!.textContent = formatNumber(stats.totalOffers);
|
||||
selectors.openCapacity!.textContent = `${formatNumber(stats.openCapacity)} units`;
|
||||
selectors.averagePrice!.textContent = `${formatNumber(stats.averagePrice, {
|
||||
selectors.totalOffers.textContent = formatNumber(stats.totalOffers);
|
||||
selectors.openCapacity.textContent = `${formatNumber(stats.openCapacity)} units`;
|
||||
selectors.averagePrice.textContent = `${formatNumber(stats.averagePrice, {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
})} credits`;
|
||||
selectors.activeBids!.textContent = formatNumber(stats.activeBids);
|
||||
selectors.activeBids.textContent = formatNumber(stats.activeBids);
|
||||
}
|
||||
|
||||
function statusClass(status: string): string {
|
||||
@@ -115,12 +135,11 @@ function statusClass(status: string): string {
|
||||
}
|
||||
|
||||
function renderOffers(offers: MarketplaceOffer[]): void {
|
||||
if (!selectors.offersWrapper) {
|
||||
return;
|
||||
}
|
||||
const wrapper = selectors.offersWrapper;
|
||||
if (!wrapper) return;
|
||||
|
||||
if (offers.length === 0) {
|
||||
selectors.offersWrapper.innerHTML = '<p class="empty-state">No offers available right now. Check back soon or submit a bid.</p>';
|
||||
wrapper.innerHTML = '<p class="empty-state">No offers available right now. Check back soon or submit a bid.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -157,7 +176,7 @@ function renderOffers(offers: MarketplaceOffer[]): void {
|
||||
</div>
|
||||
<div class="offer-models">
|
||||
<span class="models-label">Available Models</span>
|
||||
<div class="model-tags">${offer.attributes.models.map(m => `<span class="model-tag">${m}</span>`).join('')}</div>
|
||||
<div class="model-tags">${offer.attributes.models.map((m: string) => `<span class="model-tag">${m}</span>`).join('')}</div>
|
||||
</div>` : ''}
|
||||
<div class="offer-pricing">
|
||||
<div class="offer-price">${formatNumber(offer.price_per_hour ?? offer.price, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} <small>credits/hr</small></div>
|
||||
@@ -168,7 +187,7 @@ function renderOffers(offers: MarketplaceOffer[]): void {
|
||||
)
|
||||
.join('');
|
||||
|
||||
selectors.offersWrapper.innerHTML = `<div class="offers-grid">${cards}</div>`;
|
||||
wrapper.innerHTML = `<div class="offers-grid">${cards}</div>`;
|
||||
}
|
||||
|
||||
function showToast(message: string, duration = 2500): void {
|
||||
@@ -192,8 +211,9 @@ async function loadDashboard(): Promise<void> {
|
||||
renderOffers(offers);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
if (selectors.offersWrapper) {
|
||||
selectors.offersWrapper.innerHTML = '<p class="empty-state">Failed to load offers. Please retry shortly.</p>';
|
||||
const wrapper = selectors.offersWrapper;
|
||||
if (wrapper) {
|
||||
wrapper.innerHTML = '<p class="empty-state">Failed to load offers. Please retry shortly.</p>';
|
||||
}
|
||||
showToast('Failed to load marketplace data.');
|
||||
}
|
||||
@@ -202,7 +222,10 @@ async function loadDashboard(): Promise<void> {
|
||||
selectors.bidForm?.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const formData = new FormData(selectors.bidForm!);
|
||||
const form = selectors.bidForm;
|
||||
if (!form) return;
|
||||
|
||||
const formData = new FormData(form);
|
||||
const provider = formData.get('provider')?.toString().trim();
|
||||
const capacity = Number(formData.get('capacity'));
|
||||
const price = Number(formData.get('price'));
|
||||
@@ -214,16 +237,109 @@ selectors.bidForm?.addEventListener('submit', async (event) => {
|
||||
}
|
||||
|
||||
try {
|
||||
selectors.bidForm!.querySelector('button')!.setAttribute('disabled', 'disabled');
|
||||
const submitButton = form.querySelector('button');
|
||||
if (submitButton) {
|
||||
submitButton.setAttribute('disabled', 'disabled');
|
||||
}
|
||||
|
||||
await submitMarketplaceBid({ provider, capacity, price, notes });
|
||||
selectors.bidForm!.reset();
|
||||
form.reset();
|
||||
showToast('Bid submitted successfully!');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
showToast('Unable to submit bid. Please try again.');
|
||||
} finally {
|
||||
selectors.bidForm!.querySelector('button')!.removeAttribute('disabled');
|
||||
const submitButton = form.querySelector('button');
|
||||
if (submitButton) {
|
||||
submitButton.removeAttribute('disabled');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
loadDashboard();
|
||||
|
||||
// Dark mode functionality with system preference detection
|
||||
function toggleDarkMode() {
|
||||
const currentTheme = document.documentElement.classList.contains('dark') ? 'dark' : 'light';
|
||||
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
||||
setTheme(newTheme);
|
||||
}
|
||||
|
||||
function setTheme(theme: string) {
|
||||
// Apply theme immediately
|
||||
if (theme === 'dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
|
||||
// Save to localStorage for persistence
|
||||
localStorage.setItem('marketplaceTheme', theme);
|
||||
|
||||
// Update button display
|
||||
updateThemeButton(theme);
|
||||
|
||||
// Send analytics event if available
|
||||
if (typeof window !== 'undefined' && window.analytics) {
|
||||
window.analytics.track('marketplace_theme_changed', { theme });
|
||||
}
|
||||
}
|
||||
|
||||
function updateThemeButton(theme: string) {
|
||||
const emoji = document.getElementById('darkModeEmoji');
|
||||
const text = document.getElementById('darkModeText');
|
||||
|
||||
if (emoji && text) {
|
||||
if (theme === 'dark') {
|
||||
emoji.textContent = '🌙';
|
||||
text.textContent = 'Dark';
|
||||
} else {
|
||||
emoji.textContent = '☀️';
|
||||
text.textContent = 'Light';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getPreferredTheme(): string {
|
||||
// 1. Check localStorage first (user preference for marketplace)
|
||||
const saved = localStorage.getItem('marketplaceTheme');
|
||||
if (saved) {
|
||||
return saved;
|
||||
}
|
||||
|
||||
// 2. Check main site preference for consistency
|
||||
const mainSiteTheme = localStorage.getItem('theme');
|
||||
if (mainSiteTheme) {
|
||||
return mainSiteTheme;
|
||||
}
|
||||
|
||||
// 3. Check system preference
|
||||
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
return 'dark';
|
||||
}
|
||||
|
||||
// 4. Default to dark (AITBC brand preference)
|
||||
return 'dark';
|
||||
}
|
||||
|
||||
function initializeTheme() {
|
||||
const theme = getPreferredTheme();
|
||||
setTheme(theme);
|
||||
|
||||
// Listen for system preference changes
|
||||
if (window.matchMedia) {
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
|
||||
// Only auto-switch if user hasn't manually set a preference
|
||||
if (!localStorage.getItem('marketplaceTheme') && !localStorage.getItem('theme')) {
|
||||
setTheme(e.matches ? 'dark' : 'light');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize theme immediately (before DOM loads)
|
||||
initializeTheme();
|
||||
|
||||
// Reference to suppress TypeScript "never used" warning
|
||||
// @ts-ignore - function called from HTML onclick
|
||||
window.toggleDarkMode = toggleDarkMode;
|
||||
|
||||
@@ -8,10 +8,29 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* Dark mode variables */
|
||||
.dark {
|
||||
--bg-primary: #1f2937;
|
||||
--bg-secondary: #374151;
|
||||
--bg-card: #111827;
|
||||
--text-primary: #f9fafb;
|
||||
--text-secondary: #d1d5db;
|
||||
--text-muted: #9ca3af;
|
||||
--border-color: #4b5563;
|
||||
--hover-bg: #374151;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(180deg, #f7f8fa 0%, #eef1f6 100%);
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
/* Dark mode body */
|
||||
.dark body {
|
||||
background: linear-gradient(180deg, #1f2937 0%, #111827 100%);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
#app {
|
||||
@@ -24,17 +43,118 @@ body {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.page-header__nav {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.page-header__nav a {
|
||||
font-size: 0.85rem;
|
||||
padding: 0.3rem 0.75rem;
|
||||
border-radius: 999px;
|
||||
color: #5a6575;
|
||||
text-decoration: none;
|
||||
transition: background 150ms ease, color 150ms ease;
|
||||
}
|
||||
|
||||
.dark .page-header__nav a {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.page-header__nav a:hover {
|
||||
background: rgba(37, 99, 235, 0.08);
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.dark .page-header__nav a:hover {
|
||||
background: rgba(37, 99, 235, 0.15);
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
.page-header__nav .back-link {
|
||||
border: 1px solid #d1d5db;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.dark .page-header__nav .back-link {
|
||||
border-color: var(--border-color);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.page-header__nav .back-link:hover {
|
||||
border-color: #2563eb;
|
||||
color: #2563eb;
|
||||
background: rgba(37, 99, 235, 0.06);
|
||||
}
|
||||
|
||||
.dark .page-header__nav .back-link:hover {
|
||||
border-color: #60a5fa;
|
||||
color: #60a5fa;
|
||||
background: rgba(37, 99, 235, 0.12);
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-size: 2.4rem;
|
||||
margin: 0 0 0.5rem;
|
||||
color: #1d2736;
|
||||
}
|
||||
|
||||
.dark .page-header h1 {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
margin: 0;
|
||||
color: #5a6575;
|
||||
}
|
||||
|
||||
.dark .page-header p {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.page-header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.page-header-title {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.dark-mode-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
border: 2px solid #2563eb;
|
||||
border-radius: 999px;
|
||||
color: #2563eb;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.dark .dark-mode-toggle {
|
||||
background: rgba(59, 130, 246, 0.15);
|
||||
border-color: #60a5fa;
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
.dark-mode-toggle:hover {
|
||||
background: rgba(59, 130, 246, 0.15);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.dark .dark-mode-toggle:hover {
|
||||
background: rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
@@ -47,6 +167,12 @@ body {
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 12px 24px rgba(18, 24, 32, 0.08);
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.dark .stat-card {
|
||||
background: var(--bg-card);
|
||||
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.stat-card h2 {
|
||||
@@ -55,11 +181,19 @@ body {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.dark .stat-card h2 {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.stat-card strong {
|
||||
font-size: 1.8rem;
|
||||
color: #1d2736;
|
||||
}
|
||||
|
||||
.dark .stat-card strong {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.stat-card span {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
@@ -67,9 +201,8 @@ body {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.panels {
|
||||
display: grid;
|
||||
gap: 24px;
|
||||
.dark .stat-card span {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.panel {
|
||||
@@ -77,6 +210,12 @@ body {
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 10px 20px rgba(15, 23, 42, 0.08);
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.dark .panel {
|
||||
background: var(--bg-card);
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.panel h2 {
|
||||
@@ -85,6 +224,10 @@ body {
|
||||
color: #1d2736;
|
||||
}
|
||||
|
||||
.dark .panel h2 {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.offers-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
@@ -97,6 +240,11 @@ body {
|
||||
border-bottom: 1px solid #e5e9f1;
|
||||
}
|
||||
|
||||
.dark .offers-table th,
|
||||
.dark .offers-table td {
|
||||
border-bottom-color: var(--border-color);
|
||||
}
|
||||
|
||||
.offers-table th {
|
||||
color: #64748b;
|
||||
font-size: 0.85rem;
|
||||
@@ -104,18 +252,16 @@ body {
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.dark .offers-table th {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.offers-table tbody tr:hover {
|
||||
background-color: rgba(99, 102, 241, 0.08);
|
||||
}
|
||||
|
||||
.table-responsive {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.offers-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 20px;
|
||||
.dark .offers-table tbody tr:hover {
|
||||
background-color: var(--hover-bg);
|
||||
}
|
||||
|
||||
.offer-card {
|
||||
@@ -126,30 +272,36 @@ body {
|
||||
transition: box-shadow 200ms ease, transform 200ms ease;
|
||||
}
|
||||
|
||||
.dark .offer-card {
|
||||
background: var(--bg-card);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
.offer-card:hover {
|
||||
box-shadow: 0 8px 24px rgba(99, 102, 241, 0.12);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.offer-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.offer-gpu-name {
|
||||
font-size: 1.15rem;
|
||||
font-weight: 700;
|
||||
color: #1d2736;
|
||||
}
|
||||
|
||||
.dark .offer-gpu-name {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.offer-provider {
|
||||
font-size: 0.85rem;
|
||||
color: #64748b;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.dark .offer-provider {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.offer-specs {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
@@ -160,8 +312,8 @@ body {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.spec-item {
|
||||
text-align: center;
|
||||
.dark .offer-specs {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.spec-label {
|
||||
@@ -173,6 +325,10 @@ body {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.dark .spec-label {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.spec-value {
|
||||
display: block;
|
||||
font-size: 0.95rem;
|
||||
@@ -180,10 +336,8 @@ body {
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.offer-pricing {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
.dark .spec-value {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.offer-price {
|
||||
@@ -192,52 +346,27 @@ body {
|
||||
color: #6366f1;
|
||||
}
|
||||
|
||||
.dark .offer-price {
|
||||
color: #a78bfa;
|
||||
}
|
||||
|
||||
.offer-price small {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.dark .offer-price small {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.offer-sla {
|
||||
font-size: 0.8rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.offer-plugins {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.plugin-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 3px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.offer-models {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.models-label {
|
||||
display: block;
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.model-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
.dark .offer-sla {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.model-tag {
|
||||
@@ -251,6 +380,12 @@ body {
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.dark .model-tag {
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
.status-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -271,6 +406,11 @@ body {
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.dark .status-reserved {
|
||||
background-color: rgba(59, 130, 246, 0.2);
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.bid-form {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
@@ -283,6 +423,10 @@ body {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.dark .bid-form label {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.bid-form input,
|
||||
.bid-form select,
|
||||
.bid-form textarea {
|
||||
@@ -295,6 +439,14 @@ body {
|
||||
background-color: #f9fbff;
|
||||
}
|
||||
|
||||
.dark .bid-form input,
|
||||
.dark .bid-form select,
|
||||
.dark .bid-form textarea {
|
||||
border-color: var(--border-color);
|
||||
background-color: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.bid-form button {
|
||||
justify-self: flex-start;
|
||||
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
||||
@@ -322,6 +474,12 @@ body {
|
||||
background-color: rgba(99, 102, 241, 0.05);
|
||||
}
|
||||
|
||||
.dark .empty-state {
|
||||
color: var(--text-secondary);
|
||||
border-color: var(--border-color);
|
||||
background-color: rgba(99, 102, 241, 0.1);
|
||||
}
|
||||
|
||||
.toast {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
@@ -336,6 +494,11 @@ body {
|
||||
transition: opacity 200ms ease, transform 200ms ease;
|
||||
}
|
||||
|
||||
.dark .toast {
|
||||
background: var(--bg-card);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.toast.visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
|
||||
Reference in New Issue
Block a user