merge: resolve openapi-client-gen conflicts with CI typecheck script

Keeps both generate scripts and typecheck script in package.json.
This commit is contained in:
agent-company
2026-03-26 12:13:08 +00:00
28 changed files with 1440 additions and 73 deletions
+9
View File
@@ -7,6 +7,15 @@
<title>SPARC Dashboard</title>
</head>
<body>
<script>
// Prevent FOUC: apply saved theme before first render
(function() {
var theme = localStorage.getItem('theme');
if (theme === 'dark' || (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
}
})();
</script>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
+4 -4
View File
@@ -10,7 +10,7 @@
"dependencies": {
"@tanstack/react-query": "^5.51.0",
"axios": "^1.7.2",
"lucide-react": "^0.400.0",
"lucide-react": "^1.7.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.24.0",
@@ -3452,9 +3452,9 @@
}
},
"node_modules/lucide-react": {
"version": "0.400.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.400.0.tgz",
"integrity": "sha512-rpp7pFHh3Xd93KHixNgB0SqThMHpYNzsGUu69UaQbSZ75Q/J3m5t6EhKyMT3m4w2WOxmJ2mY0tD3vebnXqQryQ==",
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.7.0.tgz",
"integrity": "sha512-yI7BeItCLZJTXikmK4KNUGCKoGzSvbKlfCvw44bU4fXAL6v3gYS4uHD1jzsLkfwODYwI6Drw5Tu9Z5ulDe0TSg==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+2 -1
View File
@@ -9,12 +9,13 @@
"lint": "eslint .",
"generate": "openapi-typescript http://localhost:8000/api/openapi.json -o src/api/schema.d.ts",
"generate:local": "openapi-typescript src/api/openapi.json -o src/api/schema.d.ts",
"typecheck": "tsc --noEmit",
"preview": "vite preview"
},
"dependencies": {
"@tanstack/react-query": "^5.51.0",
"axios": "^1.7.2",
"lucide-react": "^0.400.0",
"lucide-react": "^1.7.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.24.0",
+5
View File
@@ -1,6 +1,7 @@
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { AuthProvider } from './context/AuthContext';
import { ThemeProvider } from './context/ThemeContext';
import { Layout } from './components/Layout';
import { ProtectedRoute } from './components/ProtectedRoute';
import { Login } from './pages/Login';
@@ -10,6 +11,7 @@ import { Batch } from './pages/Batch';
import { AnalyticsPage } from './pages/Analytics';
import { About } from './pages/About';
import { AdminUsers } from './pages/AdminUsers';
import { Compare } from './pages/Compare';
const queryClient = new QueryClient({
defaultOptions: {
@@ -22,6 +24,7 @@ const queryClient = new QueryClient({
function App() {
return (
<ThemeProvider>
<QueryClientProvider client={queryClient}>
<AuthProvider>
<BrowserRouter>
@@ -41,6 +44,7 @@ function App() {
<Route path="/analysis" element={<Analysis />} />
<Route path="/batch" element={<Batch />} />
<Route path="/analytics" element={<AnalyticsPage />} />
<Route path="/compare" element={<Compare />} />
<Route path="/about" element={<About />} />
{/* Admin routes */}
@@ -61,6 +65,7 @@ function App() {
</BrowserRouter>
</AuthProvider>
</QueryClientProvider>
</ThemeProvider>
);
}
+28
View File
@@ -126,12 +126,40 @@ export const analysisApi = {
},
};
// Export API
export const exportApi = {
exportCsv: async (companyName: string): Promise<void> => {
const response = await api.get(`/export/${encodeURIComponent(companyName)}`, {
responseType: 'blob',
});
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', `sparc_${companyName.toLowerCase().replace(/\s+/g, '_')}_export.csv`);
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(url);
},
};
// Analytics API
export interface TrendData {
by_month: Array<{ month: string; company_name: string; count: number }>;
by_type_over_time: Array<{ month: string; analysis_type: string; count: number }>;
period_days: number;
}
export const analyticsApi = {
getAnalytics: async (days = 30): Promise<Analytics> => {
const response = await api.get<Analytics>(`/analytics?days=${days}`);
return response.data;
},
getTrends: async (days = 90): Promise<TrendData> => {
const response = await api.get<TrendData>(`/analytics/trends?days=${days}`);
return response.data;
},
};
// Admin API
+12 -2
View File
@@ -1,9 +1,11 @@
import { Outlet, NavLink, useNavigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
import { Search, Layers, BarChart3, Info, Users, LogOut } from 'lucide-react';
import { useTheme } from '../context/ThemeContext';
import { Search, Layers, BarChart3, Info, Users, LogOut, GitCompareArrows, Sun, Moon } from 'lucide-react';
export function Layout() {
const { user, isAdmin, logout } = useAuth();
const { theme, toggleTheme } = useTheme();
const navigate = useNavigate();
const handleLogout = () => {
@@ -15,6 +17,7 @@ export function Layout() {
{ to: '/analysis', icon: Search, label: 'Analysis' },
{ to: '/batch', icon: Layers, label: 'Batch' },
{ to: '/analytics', icon: BarChart3, label: 'Analytics' },
{ to: '/compare', icon: GitCompareArrows, label: 'Compare' },
{ to: '/about', icon: Info, label: 'About' },
];
@@ -23,7 +26,7 @@ export function Layout() {
}
return (
<div className="min-h-screen bg-gradient-to-br from-bg-dark to-indigo-950">
<div className="min-h-screen bg-gradient-to-br from-bg-dark to-slate-100 dark:to-indigo-950">
{/* Header */}
<header className="bg-bg-card/80 backdrop-blur-lg border-b border-primary/20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
@@ -63,6 +66,13 @@ export function Layout() {
{/* User menu */}
<div className="flex items-center gap-4">
<button
onClick={toggleTheme}
className="p-2 rounded-lg text-text-secondary hover:text-text-primary hover:bg-bg-card-hover transition-all"
aria-label={theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}
>
{theme === 'dark' ? <Sun size={18} /> : <Moon size={18} />}
</button>
<div className="text-right hidden sm:block">
<div className="text-sm font-medium text-text-primary">{user?.email}</div>
<div className="text-xs text-text-secondary capitalize">{user?.role}</div>
+1 -1
View File
@@ -12,7 +12,7 @@ export function ProtectedRoute({ children, requireAdmin = false }: ProtectedRout
if (isLoading) {
return (
<div className="min-h-screen bg-gradient-to-br from-bg-dark to-indigo-950 flex items-center justify-center">
<div className="min-h-screen bg-gradient-to-br from-bg-dark to-slate-100 dark:to-indigo-950 flex items-center justify-center">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-primary"></div>
</div>
);
+48
View File
@@ -0,0 +1,48 @@
import { createContext, useContext, useEffect, useState } from 'react';
type Theme = 'light' | 'dark';
interface ThemeContextType {
theme: Theme;
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
function getInitialTheme(): Theme {
const stored = localStorage.getItem('theme');
if (stored === 'light' || stored === 'dark') return stored;
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>(getInitialTheme);
useEffect(() => {
const root = document.documentElement;
if (theme === 'dark') {
root.classList.add('dark');
} else {
root.classList.remove('dark');
}
localStorage.setItem('theme', theme);
}, [theme]);
const toggleTheme = () => {
setTheme((prev) => (prev === 'dark' ? 'light' : 'dark'));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}
+22 -2
View File
@@ -2,6 +2,26 @@
@tailwind components;
@tailwind utilities;
/* Light mode (default) */
:root {
--color-bg-dark: #f1f5f9;
--color-bg-card: #ffffff;
--color-bg-card-hover: #e2e8f0;
--color-text-primary: #0f172a;
--color-text-secondary: #475569;
--color-border: #cbd5e1;
}
/* Dark mode */
.dark {
--color-bg-dark: #0f172a;
--color-bg-card: #1e293b;
--color-bg-card-hover: #334155;
--color-text-primary: #f8fafc;
--color-text-secondary: #94a3b8;
--color-border: #334155;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
-webkit-font-smoothing: antialiased;
@@ -15,7 +35,7 @@ body {
}
::-webkit-scrollbar-track {
background: #1e293b;
background: var(--color-bg-card);
}
::-webkit-scrollbar-thumb {
@@ -30,5 +50,5 @@ body {
/* Selection */
::selection {
background: rgba(99, 102, 241, 0.3);
color: #f8fafc;
color: var(--color-text-primary);
}
+14 -5
View File
@@ -1,7 +1,7 @@
import { useState } from 'react';
import { useMutation } from '@tanstack/react-query';
import { analysisApi } from '../api/client';
import { Search, CheckCircle, AlertCircle, Clock, FileText } from 'lucide-react';
import { analysisApi, exportApi } from '../api/client';
import { Search, CheckCircle, AlertCircle, Clock, FileText, Download } from 'lucide-react';
import type { CompanyAnalysis } from '../types';
export function Analysis() {
@@ -106,9 +106,18 @@ export function Analysis() {
{/* Analysis Content */}
{result.success && result.analysis && (
<div className="bg-bg-card/60 backdrop-blur-lg border border-primary/15 rounded-2xl p-6">
<h3 className="text-lg font-semibold text-text-primary border-b-2 border-primary/30 pb-2 mb-4">
AI Analysis Results
</h3>
<div className="flex items-center justify-between border-b-2 border-primary/30 pb-2 mb-4">
<h3 className="text-lg font-semibold text-text-primary">
AI Analysis Results
</h3>
<button
onClick={() => exportApi.exportCsv(result.company_name)}
className="flex items-center gap-2 text-sm bg-primary/20 hover:bg-primary/30 text-primary font-medium px-3 py-1.5 rounded-lg transition-colors"
>
<Download size={14} />
Export CSV
</button>
</div>
<div className="prose prose-invert max-w-none">
<div className="text-text-primary whitespace-pre-wrap leading-relaxed">
{result.analysis}
+149 -10
View File
@@ -2,22 +2,50 @@ import { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { analyticsApi } from '../api/client';
import { AlertCircle, Database } from 'lucide-react';
import { PieChart, Pie, Cell, BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Legend } from 'recharts';
import { PieChart, Pie, Cell, BarChart, Bar, LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, Legend } from 'recharts';
const COLORS = ['#6366f1', '#0ea5e9', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#14b8a6'];
export function AnalyticsPage() {
const [days, setDays] = useState(30);
const { data, isLoading, isError } = useQuery({
const { data, isLoading, isError, refetch } = useQuery({
queryKey: ['analytics', days],
queryFn: () => analyticsApi.getAnalytics(days),
});
const trendsQuery = useQuery({
queryKey: ['analytics-trends', days],
queryFn: () => analyticsApi.getTrends(days),
});
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-primary"></div>
<div className="space-y-6">
<div>
<h2 className="text-xl font-semibold text-text-primary border-b-2 border-primary/30 pb-2 mb-2">
Analytics Dashboard
</h2>
<p className="text-text-secondary">Loading analytics data...</p>
</div>
{/* Skeleton cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{[1, 2, 3].map((i) => (
<div key={i} className="bg-gradient-to-br from-primary/10 to-secondary/10 border border-primary/20 rounded-xl p-5 text-center animate-pulse">
<div className="h-9 w-16 bg-primary/20 rounded mx-auto mb-2" />
<div className="h-4 w-24 bg-primary/10 rounded mx-auto" />
</div>
))}
</div>
{/* Skeleton charts */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{[1, 2].map((i) => (
<div key={i} className="bg-bg-card/60 border border-primary/15 rounded-2xl p-6 animate-pulse">
<div className="h-5 w-40 bg-primary/20 rounded mb-4" />
<div className="h-[300px] bg-primary/5 rounded" />
</div>
))}
</div>
</div>
);
}
@@ -33,15 +61,18 @@ export function AnalyticsPage() {
<div className="bg-gradient-to-br from-primary/10 to-secondary/5 border border-primary/20 rounded-xl p-6">
<div className="flex items-center gap-3 text-warning mb-2">
<Database size={24} />
<span className="font-semibold">Database Not Connected</span>
<span className="font-semibold">Unable to Load Analytics</span>
</div>
<p className="text-text-secondary">
Set <code className="bg-bg-card px-2 py-1 rounded">USE_DATABASE=true</code> in your .env file to enable analytics tracking.
Could not connect to the analytics database. Ensure PostgreSQL is running and
<code className="bg-bg-card px-2 py-1 rounded mx-1">DATABASE_URL</code> is configured correctly.
</p>
</div>
<div className="flex items-center gap-2 bg-secondary/10 border border-secondary/20 text-secondary rounded-xl px-4 py-3">
<AlertCircle size={18} />
<span>Analytics features require storing analysis results in PostgreSQL for historical tracking.</span>
<button
onClick={() => refetch()}
className="mt-3 text-sm bg-primary/20 hover:bg-primary/30 text-primary font-medium px-4 py-2 rounded-lg transition-colors"
>
Retry
</button>
</div>
</div>
);
@@ -163,6 +194,114 @@ export function AnalyticsPage() {
</div>
)}
</div>
{/* Trend Charts */}
{trendsQuery.data && (
<div className="space-y-6">
<h3 className="text-lg font-semibold text-text-primary border-b-2 border-primary/30 pb-2">
Trends Over Time
</h3>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Patent count over time per company (line chart) */}
{trendsQuery.data.by_month.length > 0 && (() => {
// Pivot data: each month as a row, companies as columns
const companies = [...new Set(trendsQuery.data!.by_month.map(d => d.company_name))];
const months = [...new Set(trendsQuery.data!.by_month.map(d => d.month))].sort();
const pivoted = months.map(month => {
const row: Record<string, string | number> = { month };
for (const c of companies) {
const entry = trendsQuery.data!.by_month.find(d => d.month === month && d.company_name === c);
row[c] = entry?.count || 0;
}
return row;
});
return (
<div className="bg-bg-card/60 border border-primary/15 rounded-2xl p-6">
<h4 className="text-md font-semibold text-text-primary mb-4">Analyses per Company Over Time</h4>
<ResponsiveContainer width="100%" height={300}>
<LineChart data={pivoted}>
<XAxis dataKey="month" stroke="#94a3b8" fontSize={12} />
<YAxis stroke="#94a3b8" fontSize={12} />
<Tooltip
contentStyle={{
backgroundColor: '#1e293b',
border: '1px solid rgba(99, 102, 241, 0.3)',
borderRadius: '8px',
}}
labelStyle={{ color: '#f8fafc' }}
/>
<Legend />
{companies.map((company, idx) => (
<Line
key={company}
type="monotone"
dataKey={company}
stroke={COLORS[idx % COLORS.length]}
strokeWidth={2}
dot={{ r: 4 }}
name={company.toUpperCase()}
/>
))}
</LineChart>
</ResponsiveContainer>
</div>
);
})()}
{/* Analysis type distribution over time (stacked bar) */}
{trendsQuery.data.by_type_over_time.length > 0 && (() => {
const types = [...new Set(trendsQuery.data!.by_type_over_time.map(d => d.analysis_type))];
const months = [...new Set(trendsQuery.data!.by_type_over_time.map(d => d.month))].sort();
const pivoted = months.map(month => {
const row: Record<string, string | number> = { month };
for (const t of types) {
const entry = trendsQuery.data!.by_type_over_time.find(d => d.month === month && d.analysis_type === t);
row[t] = entry?.count || 0;
}
return row;
});
return (
<div className="bg-bg-card/60 border border-primary/15 rounded-2xl p-6">
<h4 className="text-md font-semibold text-text-primary mb-4">Analysis Types Over Time</h4>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={pivoted}>
<XAxis dataKey="month" stroke="#94a3b8" fontSize={12} />
<YAxis stroke="#94a3b8" fontSize={12} />
<Tooltip
contentStyle={{
backgroundColor: '#1e293b',
border: '1px solid rgba(99, 102, 241, 0.3)',
borderRadius: '8px',
}}
labelStyle={{ color: '#f8fafc' }}
/>
<Legend />
{types.map((type, idx) => (
<Bar
key={type}
dataKey={type}
stackId="types"
fill={COLORS[idx % COLORS.length]}
name={type}
/>
))}
</BarChart>
</ResponsiveContainer>
</div>
);
})()}
</div>
{trendsQuery.data.by_month.length === 0 && (
<div className="text-text-secondary text-center py-8">
No trend data available yet. Run analyses over multiple days to see trends.
</div>
)}
</div>
)}
</div>
);
}
+15 -3
View File
@@ -114,9 +114,21 @@ export function Batch() {
{/* Error */}
{mutation.isError && (
<div className="flex items-center gap-2 bg-error/10 border border-error/20 text-error rounded-xl px-4 py-3">
<AlertCircle size={18} />
<span>Batch analysis failed. Please try again.</span>
<div className="bg-error/10 border border-error/20 rounded-xl px-4 py-3">
<div className="flex items-center gap-2 text-error">
<AlertCircle size={18} />
<span className="font-semibold">Batch analysis failed</span>
</div>
<p className="text-text-secondary text-sm mt-1 ml-7">
{mutation.error instanceof Error ? mutation.error.message : 'An unexpected error occurred.'}
{' '}Check your connection and try again.
</p>
<button
onClick={() => mutation.reset()}
className="ml-7 mt-2 text-sm text-primary hover:text-primary-dark underline"
>
Dismiss
</button>
</div>
)}
+161
View File
@@ -0,0 +1,161 @@
import { useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import { analysisApi } from '../api/client';
import { GitCompareArrows, AlertCircle, FileText, Clock } from 'lucide-react';
import type { CompanyAnalysis } from '../types';
function CompanyPanel({ data, isLoading, isError }: { data?: CompanyAnalysis; isLoading: boolean; isError: boolean }) {
if (isLoading) {
return (
<div className="bg-bg-card/60 border border-primary/15 rounded-2xl p-6 animate-pulse">
<div className="h-6 w-32 bg-primary/20 rounded mb-4" />
<div className="space-y-3">
<div className="h-4 bg-primary/10 rounded w-full" />
<div className="h-4 bg-primary/10 rounded w-3/4" />
<div className="h-4 bg-primary/10 rounded w-5/6" />
</div>
</div>
);
}
if (isError) {
return (
<div className="bg-error/10 border border-error/20 rounded-2xl p-6">
<div className="flex items-center gap-2 text-error">
<AlertCircle size={18} />
<span>Failed to load analysis. Check the company name and try again.</span>
</div>
</div>
);
}
if (!data) return null;
return (
<div className="bg-bg-card/60 border border-primary/15 rounded-2xl p-6 space-y-4">
<h3 className="text-lg font-bold text-text-primary border-b-2 border-primary/30 pb-2">
{data.company_name.toUpperCase()}
</h3>
<div className="grid grid-cols-2 gap-3">
<div className="bg-primary/10 rounded-lg p-3 text-center">
<FileText className="mx-auto mb-1 text-primary" size={18} />
<div className="text-xl font-bold text-text-primary">{data.patent_count}</div>
<div className="text-xs text-text-secondary uppercase">Patents</div>
</div>
<div className="bg-primary/10 rounded-lg p-3 text-center">
<Clock className="mx-auto mb-1 text-primary" size={18} />
<div className="text-sm font-medium text-text-primary">
{new Date(data.timestamp).toLocaleDateString()}
</div>
<div className="text-xs text-text-secondary uppercase">Analyzed</div>
</div>
</div>
{data.success && data.analysis ? (
<div className="text-text-primary whitespace-pre-wrap leading-relaxed text-sm">
{data.analysis}
</div>
) : (
<div className="text-error text-sm">{data.error || 'Analysis not available'}</div>
)}
</div>
);
}
export function Compare() {
const [searchParams, setSearchParams] = useSearchParams();
const [companyA, setCompanyA] = useState(searchParams.get('a') || '');
const [companyB, setCompanyB] = useState(searchParams.get('b') || '');
const queryA = searchParams.get('a') || '';
const queryB = searchParams.get('b') || '';
const resultA = useQuery({
queryKey: ['analyze', queryA],
queryFn: () => analysisApi.analyzeCompany(queryA),
enabled: !!queryA,
});
const resultB = useQuery({
queryKey: ['analyze', queryB],
queryFn: () => analysisApi.analyzeCompany(queryB),
enabled: !!queryB,
});
const handleCompare = (e: React.FormEvent) => {
e.preventDefault();
const a = companyA.trim();
const b = companyB.trim();
if (a && b) {
setSearchParams({ a, b });
}
};
return (
<div className="space-y-6">
{/* Header */}
<div>
<h2 className="text-xl font-semibold text-text-primary border-b-2 border-primary/30 pb-2 mb-2">
Portfolio Comparison
</h2>
<p className="text-text-secondary">
Compare patent portfolios of two companies side by side.
</p>
</div>
{/* Input Form */}
<form onSubmit={handleCompare} className="flex flex-col sm:flex-row gap-3 items-end">
<div className="flex-1">
<label className="block text-sm font-medium text-text-secondary mb-1">Company A</label>
<input
type="text"
value={companyA}
onChange={(e) => setCompanyA(e.target.value)}
placeholder="e.g. nvidia"
className="w-full bg-bg-card/80 border border-primary/30 rounded-xl px-4 py-2.5 text-text-primary placeholder-text-secondary/50 focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all"
/>
</div>
<div className="flex-1">
<label className="block text-sm font-medium text-text-secondary mb-1">Company B</label>
<input
type="text"
value={companyB}
onChange={(e) => setCompanyB(e.target.value)}
placeholder="e.g. intel"
className="w-full bg-bg-card/80 border border-primary/30 rounded-xl px-4 py-2.5 text-text-primary placeholder-text-secondary/50 focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all"
/>
</div>
<button
type="submit"
disabled={!companyA.trim() || !companyB.trim() || resultA.isLoading || resultB.isLoading}
className="bg-gradient-to-r from-primary to-primary-dark text-white font-semibold py-2.5 px-6 rounded-xl hover:shadow-lg hover:shadow-primary/30 transition-all disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
>
<GitCompareArrows size={18} />
Compare
</button>
</form>
{/* Comparison Panels */}
{(queryA || queryB) && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{queryA && (
<CompanyPanel
data={resultA.data}
isLoading={resultA.isLoading}
isError={resultA.isError}
/>
)}
{queryB && (
<CompanyPanel
data={resultB.data}
isLoading={resultB.isLoading}
isError={resultB.isError}
/>
)}
</div>
)}
</div>
);
}
+1 -1
View File
@@ -31,7 +31,7 @@ export function Login() {
};
return (
<div className="min-h-screen bg-gradient-to-br from-bg-dark to-indigo-950 flex items-center justify-center px-4">
<div className="min-h-screen bg-gradient-to-br from-bg-dark to-slate-100 dark:to-indigo-950 flex items-center justify-center px-4">
<div className="w-full max-w-md">
{/* Brand */}
<div className="text-center mb-8">
+1 -1
View File
@@ -40,7 +40,7 @@ export function Register() {
};
return (
<div className="min-h-screen bg-gradient-to-br from-bg-dark to-indigo-950 flex items-center justify-center px-4">
<div className="min-h-screen bg-gradient-to-br from-bg-dark to-slate-100 dark:to-indigo-950 flex items-center justify-center px-4">
<div className="w-full max-w-md">
{/* Brand */}
<div className="text-center mb-8">
+7 -6
View File
@@ -4,6 +4,7 @@ export default {
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
darkMode: 'class',
theme: {
extend: {
colors: {
@@ -16,15 +17,15 @@ export default {
warning: '#f59e0b',
error: '#ef4444',
bg: {
dark: '#0f172a',
card: '#1e293b',
'card-hover': '#334155',
dark: 'var(--color-bg-dark)',
card: 'var(--color-bg-card)',
'card-hover': 'var(--color-bg-card-hover)',
},
text: {
primary: '#f8fafc',
secondary: '#94a3b8',
primary: 'var(--color-text-primary)',
secondary: 'var(--color-text-secondary)',
},
border: '#334155',
border: 'var(--color-border)',
},
},
},