styles restructuring
This commit is contained in:
parent
062dea1ba6
commit
d3eeaa7f40
12 changed files with 638 additions and 428 deletions
|
|
@ -4,6 +4,9 @@
|
|||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Chivo:ital,wght@0,100..900;1,100..900&family=Russo+One&family=Tektur:wght@400..900&display=swap" rel="stylesheet">
|
||||
<title>claudash</title>
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -1,42 +1,376 @@
|
|||
#root {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
/* CSS Reset - replaces MUI CssBaseline */
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
}
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
.logo.react:hover {
|
||||
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@keyframes logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
html {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
a:nth-of-type(2) .logo {
|
||||
animation: logo-spin infinite 20s linear;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
line-height: 1.5;
|
||||
font-synthesis: none;
|
||||
font-weight: 400;
|
||||
font-family: "Chivo", sans-serif !important;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: "Russo One", sans-serif !important;
|
||||
font-size:1.2rem;
|
||||
}
|
||||
/* App theme containers */
|
||||
.app-dark {
|
||||
min-height: 100vh;
|
||||
background-color: #0a1929;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
}
|
||||
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
.app-light {
|
||||
min-height: 100vh;
|
||||
background-color: #ffffff;
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
}
|
||||
.widget-grid {
|
||||
display:flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
.widget-row {
|
||||
display:flex;
|
||||
justify-content: flex-start;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
}
|
||||
.widget-row.condensed {
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.widget-row.condensed .news-widget{
|
||||
flex-grow: 2;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #90caf9;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #64b5f6;
|
||||
}
|
||||
|
||||
.app-light a {
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.app-light a:hover {
|
||||
color: #1565c0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
.app-light button {
|
||||
background-color: #f5f5f5;
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
}
|
||||
|
||||
/* Input/TextField resets - form elements need explicit inheritance */
|
||||
input, textarea {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
/* Chat Interface Styles */
|
||||
.chat-header {
|
||||
margin-left: 8px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.chat-placeholder {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 0.875rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.app-light .chat-placeholder {
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.chat-message-user {
|
||||
display: inline-block;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
max-width: 70%;
|
||||
background-color: #1976d2;
|
||||
color: white;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.chat-message-assistant {
|
||||
display: inline-block;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
max-width: 70%;
|
||||
background-color: #424242;
|
||||
color: white;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.app-light .chat-message-assistant {
|
||||
background-color: #e0e0e0;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
/* Dashboard Styles */
|
||||
.loading-status {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 1rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.app-light .loading-status {
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.token-usage-box {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background-color: #1e2a35;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.app-light .token-usage-box {
|
||||
background-color: #ffffff;
|
||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.token-usage-text {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 0.875rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.app-light .token-usage-text {
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.token-usage-caption {
|
||||
color: rgba(255, 255, 255, 0.38);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.app-light .token-usage-caption {
|
||||
color: rgba(0, 0, 0, 0.38);
|
||||
}
|
||||
|
||||
.cache-info-box {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background-color: #2e7d32;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #1b5e20;
|
||||
}
|
||||
|
||||
.app-light .cache-info-box {
|
||||
background-color: #4caf50;
|
||||
color: white;
|
||||
border: 1px solid #388e3c;
|
||||
}
|
||||
|
||||
.cache-info-text {
|
||||
font-size: 0.875rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.cache-info-caption {
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* Widget Styles */
|
||||
.widget-title {
|
||||
font-size: 2rem;
|
||||
font-weight: 500;
|
||||
margin: 0 0 8px 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.widget-error {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 0.875rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.app-light .widget-error {
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.widget-info {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 0.875rem;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.app-light .widget-info {
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.widget-caption {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 0.75rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.app-light .widget-caption {
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
/* Briefing Widget Styles */
|
||||
.briefing-text {
|
||||
white-space: pre-line;
|
||||
line-height: 1.7;
|
||||
font-size: 1.05rem;
|
||||
margin: 8px 0 0 0;
|
||||
}
|
||||
|
||||
/* Weather Widget Styles */
|
||||
.weather-temp {
|
||||
font-size: 3rem;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.weather-unit {
|
||||
font-size: 1.5rem;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.app-light .weather-unit {
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.weather-description {
|
||||
font-size: 1rem;
|
||||
margin: 0 0 16px 0;
|
||||
}
|
||||
|
||||
.forecast-label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.app-light .forecast-label {
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.forecast-day {
|
||||
font-size: 0.75rem;
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.forecast-temp {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* News Widget Styles */
|
||||
.news-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.news-author {
|
||||
font-size: 0.75rem;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
margin-left: 8px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.app-light .news-author {
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.news-date {
|
||||
font-size: 0.75rem;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.app-light .news-date {
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.news-description {
|
||||
font-size: 0.75rem;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
display: -webkit-box;
|
||||
margin-top: 4px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.app-light .news-description {
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
/* Debug Panel Styles */
|
||||
.debug-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 500;
|
||||
margin: 0 0 16px 0;
|
||||
}
|
||||
|
||||
.debug-warning {
|
||||
color: #ed6c02;
|
||||
font-size: 0.75rem;
|
||||
margin-top: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.app-light .debug-warning {
|
||||
color: #e65100;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { ThemeProvider, CssBaseline } from '@mui/material';
|
||||
import { ThemeProvider } from '@mui/material';
|
||||
import { useState } from 'react';
|
||||
import Dashboard from './components/Dashboard';
|
||||
import DebugPanel from './components/DebugPanel';
|
||||
import { lightTheme, darkTheme } from './theme';
|
||||
import './App.css';
|
||||
|
||||
function App() {
|
||||
const [isDark, setIsDark] = useState(true);
|
||||
|
|
@ -10,14 +11,15 @@ function App() {
|
|||
|
||||
return (
|
||||
<ThemeProvider theme={isDark ? darkTheme : lightTheme}>
|
||||
<CssBaseline />
|
||||
<Dashboard
|
||||
themeToggle={() => setIsDark(!isDark)}
|
||||
isDark={isDark}
|
||||
/>
|
||||
{debugMode && <DebugPanel />}
|
||||
<div className={isDark ? 'app-dark' : 'app-light'}>
|
||||
<Dashboard
|
||||
themeToggle={() => setIsDark(!isDark)}
|
||||
isDark={isDark}
|
||||
/>
|
||||
{debugMode && <DebugPanel />}
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
export default App;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useState, useRef, useEffect } from 'react';
|
||||
import { Box, TextField, IconButton, Paper, Typography } from '@mui/material';
|
||||
import { Box, TextField, IconButton, Paper } from '@mui/material';
|
||||
import { Send, ExpandLess, ExpandMore } from '@mui/icons-material';
|
||||
import { sendChatMessage } from '../services/claudeService';
|
||||
|
||||
|
|
@ -66,18 +66,16 @@ function ChatInterface() {
|
|||
>
|
||||
{isExpanded ? <ExpandMore /> : <ExpandLess />}
|
||||
</IconButton>
|
||||
<Typography variant="body2" sx={{ ml: 1 }}>
|
||||
Chat with Claude
|
||||
</Typography>
|
||||
<span className="chat-header">Chat with Claude</span>
|
||||
</Box>
|
||||
|
||||
{/* Messages area - only visible when expanded */}
|
||||
{isExpanded && (
|
||||
<Box sx={{ flex: 1, overflow: 'auto', p: 2 }}>
|
||||
{messages.length === 0 && (
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
<p className="chat-placeholder">
|
||||
Ask me anything about your briefing or the news...
|
||||
</Typography>
|
||||
</p>
|
||||
)}
|
||||
{messages.map((msg, idx) => (
|
||||
<Box
|
||||
|
|
@ -87,19 +85,9 @@ function ChatInterface() {
|
|||
textAlign: msg.role === 'user' ? 'right' : 'left'
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
display: 'inline-block',
|
||||
bgcolor: msg.role === 'user' ? 'primary.main' : 'grey.800',
|
||||
color: 'white',
|
||||
p: 1.5,
|
||||
borderRadius: 2,
|
||||
maxWidth: '70%'
|
||||
}}
|
||||
>
|
||||
<div className={msg.role === 'user' ? 'chat-message-user' : 'chat-message-assistant'}>
|
||||
{msg.content}
|
||||
</Typography>
|
||||
</div>
|
||||
</Box>
|
||||
))}
|
||||
<div ref={messagesEndRef} />
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { Box, Container, CircularProgress, IconButton, Alert, Typography } from '@mui/material';
|
||||
import { Box, Container, CircularProgress, IconButton, Alert } from '@mui/material';
|
||||
import { Brightness4, Brightness7, Refresh } from '@mui/icons-material';
|
||||
import { fetchAllSources } from '../services/dataFetcher';
|
||||
import { generateBriefing } from '../services/claudeService';
|
||||
|
|
@ -10,241 +10,233 @@ import NewsWidget from './widgets/NewsWidget';
|
|||
import ChatInterface from './ChatInterface';
|
||||
|
||||
function Dashboard({ themeToggle, isDark }) {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [loadingStatus, setLoadingStatus] = useState('Initializing...');
|
||||
const [briefing, setBriefing] = useState('');
|
||||
const [sourceData, setSourceData] = useState({});
|
||||
const [error, setError] = useState(null);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [tokenUsage, setTokenUsage] = useState(null);
|
||||
const [cacheInfo, setCacheInfo] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [loadingStatus, setLoadingStatus] = useState('Initializing...');
|
||||
const [briefing, setBriefing] = useState('');
|
||||
const [sourceData, setSourceData] = useState({});
|
||||
const [error, setError] = useState(null);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [tokenUsage, setTokenUsage] = useState(null);
|
||||
const [cacheInfo, setCacheInfo] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadDashboard();
|
||||
}, []);
|
||||
|
||||
async function loadDashboard(forceRefresh = false) {
|
||||
try {
|
||||
console.log('=== loadDashboard called ===');
|
||||
console.log('Force refresh:', forceRefresh);
|
||||
setError(null);
|
||||
|
||||
// Check if we already have today's briefing (unless forcing refresh)
|
||||
if (!forceRefresh && hasTodaysBriefing()) {
|
||||
console.log('Using cached briefing from today');
|
||||
const todaysBriefing = getTodaysBriefing();
|
||||
setBriefing(todaysBriefing.briefing);
|
||||
setSourceData(todaysBriefing.sources || {});
|
||||
|
||||
// Calculate cache age
|
||||
const cacheAge = Date.now() - todaysBriefing.timestamp;
|
||||
const cacheAgeMinutes = Math.floor(cacheAge / (1000 * 60));
|
||||
|
||||
// Set cache info to display message
|
||||
setCacheInfo({
|
||||
age: cacheAgeMinutes,
|
||||
timestamp: todaysBriefing.timestamp
|
||||
});
|
||||
setTokenUsage(null); // Clear token usage when using cache
|
||||
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Fetching fresh data...');
|
||||
setLoadingStatus('Fetching weather and news sources...');
|
||||
|
||||
// Fetch all configured sources
|
||||
const data = await fetchAllSources();
|
||||
console.log('Fetched sources:', Object.keys(data.sources || {}));
|
||||
console.log('Failed sources:', data.failed);
|
||||
setSourceData(data.sources || {});
|
||||
|
||||
// If all sources failed, show error but continue
|
||||
if (data.failed > 0 && data.successful === 0) {
|
||||
setError('All data sources failed to load. Please check your internet connection and try again.');
|
||||
} else if (data.failed > 0) {
|
||||
setError(`${data.failed} data source(s) failed to load. Showing available data.`);
|
||||
}
|
||||
|
||||
// Check if API key is configured
|
||||
const apiKey = import.meta.env.REACT_APP_ANTHROPIC_API_KEY;
|
||||
console.log('API key configured:', !!apiKey);
|
||||
|
||||
if (!apiKey || apiKey === 'sk-ant-your-key-here') {
|
||||
console.warn('API key not configured properly');
|
||||
setBriefing('Please add your Anthropic API key to .env.local to generate briefings.');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get yesterday's briefing for comparison
|
||||
const previousBriefing = getPreviousBriefing(1);
|
||||
console.log('Previous briefing exists:', !!previousBriefing);
|
||||
|
||||
// Only generate briefing if we have some data
|
||||
if (Object.keys(data.sources || {}).length > 0) {
|
||||
console.log('Calling generateBriefing...');
|
||||
setLoadingStatus('Generating your personalized briefing with AI...');
|
||||
useEffect(() => {
|
||||
loadDashboard();
|
||||
}, []);
|
||||
|
||||
async function loadDashboard(forceRefresh = false) {
|
||||
try {
|
||||
// Generate today's briefing with Claude
|
||||
const result = await generateBriefing(data, previousBriefing);
|
||||
console.log('Briefing generated');
|
||||
console.log('=== loadDashboard called ===');
|
||||
console.log('Force refresh:', forceRefresh);
|
||||
setError(null);
|
||||
|
||||
// Handle result object with text and usage
|
||||
if (result && typeof result === 'object' && result.text) {
|
||||
setBriefing(result.text);
|
||||
setTokenUsage(result.usage);
|
||||
setCacheInfo(null); // Clear cache info when generating fresh briefing
|
||||
console.log('Token usage:', result.usage);
|
||||
} else if (typeof result === 'string') {
|
||||
// Fallback for string responses
|
||||
setBriefing(result);
|
||||
setCacheInfo(null);
|
||||
}
|
||||
// Check if we already have today's briefing (unless forcing refresh)
|
||||
if (!forceRefresh && hasTodaysBriefing()) {
|
||||
console.log('Using cached briefing from today');
|
||||
const todaysBriefing = getTodaysBriefing();
|
||||
setBriefing(todaysBriefing.briefing);
|
||||
setSourceData(todaysBriefing.sources || {});
|
||||
|
||||
// Save to localStorage
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
console.log('Saving briefing for:', today);
|
||||
const briefingText = result?.text || result;
|
||||
saveBriefing(today, briefingText, data.sources);
|
||||
} catch (briefingError) {
|
||||
console.error('Failed to generate briefing:', briefingError);
|
||||
setBriefing(`Unable to generate AI briefing: ${briefingError.message}\n\nYou can still view the data widgets below.`);
|
||||
// Calculate cache age
|
||||
const cacheAge = Date.now() - todaysBriefing.timestamp;
|
||||
const cacheAgeMinutes = Math.floor(cacheAge / (1000 * 60));
|
||||
|
||||
// Set cache info to display message
|
||||
setCacheInfo({
|
||||
age: cacheAgeMinutes,
|
||||
timestamp: todaysBriefing.timestamp
|
||||
});
|
||||
setTokenUsage(null); // Clear token usage when using cache
|
||||
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Fetching fresh data...');
|
||||
setLoadingStatus('Fetching weather and news sources...');
|
||||
|
||||
// Fetch all configured sources
|
||||
const data = await fetchAllSources();
|
||||
console.log('Fetched sources:', Object.keys(data.sources || {}));
|
||||
console.log('Failed sources:', data.failed);
|
||||
setSourceData(data.sources || {});
|
||||
|
||||
// If all sources failed, show error but continue
|
||||
if (data.failed > 0 && data.successful === 0) {
|
||||
setError('All data sources failed to load. Please check your internet connection and try again.');
|
||||
} else if (data.failed > 0) {
|
||||
setError(`${data.failed} data source(s) failed to load. Showing available data.`);
|
||||
}
|
||||
|
||||
// Check if API key is configured
|
||||
const apiKey = import.meta.env.REACT_APP_ANTHROPIC_API_KEY;
|
||||
console.log('API key configured:', !!apiKey);
|
||||
|
||||
if (!apiKey || apiKey === 'sk-ant-your-key-here') {
|
||||
console.warn('API key not configured properly');
|
||||
setBriefing('Please add your Anthropic API key to .env.local to generate briefings.');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get yesterday's briefing for comparison
|
||||
const previousBriefing = getPreviousBriefing(1);
|
||||
console.log('Previous briefing exists:', !!previousBriefing);
|
||||
|
||||
// Only generate briefing if we have some data
|
||||
if (Object.keys(data.sources || {}).length > 0) {
|
||||
console.log('Calling generateBriefing...');
|
||||
setLoadingStatus('Generating your personalized briefing with AI...');
|
||||
|
||||
try {
|
||||
// Generate today's briefing with Claude
|
||||
const result = await generateBriefing(data, previousBriefing);
|
||||
console.log('Briefing generated');
|
||||
|
||||
// Handle result object with text and usage
|
||||
if (result && typeof result === 'object' && result.text) {
|
||||
setBriefing(result.text);
|
||||
setTokenUsage(result.usage);
|
||||
setCacheInfo(null); // Clear cache info when generating fresh briefing
|
||||
console.log('Token usage:', result.usage);
|
||||
} else if (typeof result === 'string') {
|
||||
// Fallback for string responses
|
||||
setBriefing(result);
|
||||
setCacheInfo(null);
|
||||
}
|
||||
|
||||
// Save to localStorage
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
console.log('Saving briefing for:', today);
|
||||
const briefingText = result?.text || result;
|
||||
saveBriefing(today, briefingText, data.sources);
|
||||
} catch (briefingError) {
|
||||
console.error('Failed to generate briefing:', briefingError);
|
||||
setBriefing(`Unable to generate AI briefing: ${briefingError.message}\n\nYou can still view the data widgets below.`);
|
||||
}
|
||||
} else {
|
||||
setBriefing('No data sources loaded successfully. Please check your configuration and try again.');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Dashboard load error:', error);
|
||||
console.error('Error stack:', error.stack);
|
||||
setError(`Failed to load dashboard: ${error.message}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
}
|
||||
} else {
|
||||
setBriefing('No data sources loaded successfully. Please check your configuration and try again.');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Dashboard load error:', error);
|
||||
console.error('Error stack:', error.stack);
|
||||
setError(`Failed to load dashboard: ${error.message}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
setRefreshing(true);
|
||||
loadDashboard(true);
|
||||
};
|
||||
const handleRefresh = () => {
|
||||
setRefreshing(true);
|
||||
loadDashboard(true);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Box display="flex" flexDirection="column" justifyContent="center" alignItems="center" minHeight="100vh" gap={2}>
|
||||
<CircularProgress />
|
||||
<p className="loading-status">{loadingStatus}</p>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Box display="flex" flexDirection="column" justifyContent="center" alignItems="center" minHeight="100vh" gap={2}>
|
||||
<CircularProgress />
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
{loadingStatus}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
<Box sx={{ minHeight: '100vh', pb: 10 }}>
|
||||
{/* Top toolbar */}
|
||||
<Box sx={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
right: 0,
|
||||
p: 2,
|
||||
display: 'flex',
|
||||
gap: 1,
|
||||
zIndex: 100
|
||||
}}>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
localStorage.clear();
|
||||
window.location.reload();
|
||||
}}
|
||||
title="Clear all cache and reload"
|
||||
color="error"
|
||||
>
|
||||
🗑️
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={handleRefresh}
|
||||
disabled={refreshing}
|
||||
title="Refresh data"
|
||||
>
|
||||
<Refresh />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={themeToggle}
|
||||
title={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
|
||||
>
|
||||
{isDark ? <Brightness7 /> : <Brightness4 />}
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
return (
|
||||
<Box sx={{ minHeight: '100vh', pb: 10 }}>
|
||||
{/* Top toolbar */}
|
||||
<Box sx={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
right: 0,
|
||||
p: 2,
|
||||
display: 'flex',
|
||||
gap: 1,
|
||||
zIndex: 100
|
||||
}}>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
localStorage.clear();
|
||||
window.location.reload();
|
||||
}}
|
||||
title="Clear all cache and reload"
|
||||
color="error"
|
||||
>
|
||||
🗑️
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={handleRefresh}
|
||||
disabled={refreshing}
|
||||
title="Refresh data"
|
||||
>
|
||||
<Refresh />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={themeToggle}
|
||||
title={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
|
||||
>
|
||||
{isDark ? <Brightness7 /> : <Brightness4 />}
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Container maxWidth="xl" sx={{ pt: 4 }}>
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Container maxWidth="xl" sx={{ pt: 4 }}>
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
{tokenUsage && (
|
||||
<div className="token-usage-box">
|
||||
<p className="token-usage-text">
|
||||
<strong>API Usage:</strong> {tokenUsage.input_tokens?.toLocaleString()} input + {tokenUsage.output_tokens?.toLocaleString()} output = {(tokenUsage.input_tokens + tokenUsage.output_tokens).toLocaleString()} total tokens
|
||||
{' • '}
|
||||
<strong>Cost:</strong> ${((tokenUsage.input_tokens * 0.003 / 1000) + (tokenUsage.output_tokens * 0.015 / 1000)).toFixed(4)}
|
||||
{' '}
|
||||
<span className="token-usage-caption">
|
||||
(Claude Sonnet 4 pricing: $0.003/1K input, $0.015/1K output)
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tokenUsage && (
|
||||
<Box sx={{ mb: 2, p: 2, bgcolor: 'background.paper', borderRadius: 1, border: '1px solid', borderColor: 'divider' }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
<strong>API Usage:</strong> {tokenUsage.input_tokens?.toLocaleString()} input + {tokenUsage.output_tokens?.toLocaleString()} output = {(tokenUsage.input_tokens + tokenUsage.output_tokens).toLocaleString()} total tokens
|
||||
{' • '}
|
||||
<strong>Cost:</strong> ${((tokenUsage.input_tokens * 0.003 / 1000) + (tokenUsage.output_tokens * 0.015 / 1000)).toFixed(4)}
|
||||
{' '}
|
||||
<Typography component="span" variant="caption" color="text.disabled">
|
||||
(Claude Sonnet 4 pricing: $0.003/1K input, $0.015/1K output)
|
||||
</Typography>
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
{cacheInfo && (
|
||||
<div className="cache-info-box">
|
||||
<p className="cache-info-text">
|
||||
<strong>💾 Using cached results</strong> from {cacheInfo.age < 1 ? 'less than a minute' : cacheInfo.age === 1 ? '1 minute' : `${cacheInfo.age} minutes`} ago — no API costs incurred
|
||||
{' '}
|
||||
<span className="cache-info-caption">
|
||||
(Click refresh to generate a new briefing)
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{cacheInfo && (
|
||||
<Box sx={{ mb: 2, p: 2, bgcolor: 'success.main', color: 'success.contrastText', borderRadius: 1, border: '1px solid', borderColor: 'success.dark' }}>
|
||||
<Typography variant="body2">
|
||||
<strong>💾 Using cached results</strong> from {cacheInfo.age < 1 ? 'less than a minute' : cacheInfo.age === 1 ? '1 minute' : `${cacheInfo.age} minutes`} ago — no API costs incurred
|
||||
{' '}
|
||||
<Typography component="span" variant="caption" sx={{ opacity: 0.9 }}>
|
||||
(Click refresh to generate a new briefing)
|
||||
</Typography>
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
<Box className="widget-grid">
|
||||
<Box className="widget-row">
|
||||
<BriefingWidget briefing={briefing} />
|
||||
<WeatherWidget
|
||||
data={sourceData.weather}
|
||||
/>
|
||||
</Box>
|
||||
<Box className="widget-row condensed">
|
||||
{/* Render NewsWidget for each news source */}
|
||||
{Object.entries(sourceData).map(([key, data]) => {
|
||||
if (key === 'weather') return null;
|
||||
return (
|
||||
<NewsWidget
|
||||
key={key}
|
||||
data={data}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
</Container>
|
||||
|
||||
{/* Widget Grid - easily rearrangeable */}
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 2,
|
||||
mb: 4
|
||||
}}>
|
||||
<BriefingWidget briefing={briefing} sx={{ flex: '1 1 100%' }} />
|
||||
|
||||
<WeatherWidget
|
||||
data={sourceData.weather}
|
||||
sx={{ flex: '1 1 300px' }}
|
||||
/>
|
||||
|
||||
{/* Render NewsWidget for each news source */}
|
||||
{Object.entries(sourceData).map(([key, data]) => {
|
||||
if (key === 'weather') return null;
|
||||
return (
|
||||
<NewsWidget
|
||||
key={key}
|
||||
data={data}
|
||||
sx={{ flex: '1 1 400px' }}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{/* Fixed chat at bottom */}
|
||||
<ChatInterface />
|
||||
</Box>
|
||||
</Container>
|
||||
|
||||
{/* Fixed chat at bottom */}
|
||||
<ChatInterface />
|
||||
</Box>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
export default Dashboard;
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { useState } from 'react';
|
||||
import { Button, Box, Paper, Typography } from '@mui/material';
|
||||
import { Button, Box, Paper } from '@mui/material';
|
||||
import { Download, Upload } from '@mui/icons-material';
|
||||
import { exportAllData, importDummyData } from '../services/storageService';
|
||||
|
||||
|
|
@ -38,7 +38,7 @@ function DebugPanel() {
|
|||
|
||||
return (
|
||||
<Paper sx={{ position: 'fixed', bottom: 80, right: 16, p: 2, width: 250, zIndex: 999 }}>
|
||||
<Typography variant="h6" gutterBottom>Debug Tools</Typography>
|
||||
<h3 className="debug-title">Debug Tools</h3>
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
|
|
@ -66,13 +66,7 @@ function DebugPanel() {
|
|||
</Button>
|
||||
|
||||
{dummyMode && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="warning.main"
|
||||
sx={{ mt: 1, display: 'block' }}
|
||||
>
|
||||
Using dummy data
|
||||
</Typography>
|
||||
<span className="debug-warning">Using dummy data</span>
|
||||
)}
|
||||
</Paper>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Typography, Skeleton, Box } from '@mui/material';
|
||||
import { Skeleton, Box } from '@mui/material';
|
||||
import GenericWidget from './GenericWidget';
|
||||
|
||||
function BriefingWidget({ briefing, sx }) {
|
||||
|
|
@ -9,16 +9,7 @@ function BriefingWidget({ briefing, sx }) {
|
|||
>
|
||||
<Box sx={{ mt: 1 }}>
|
||||
{briefing ? (
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{
|
||||
whiteSpace: 'pre-line',
|
||||
lineHeight: 1.7,
|
||||
fontSize: '1.05rem'
|
||||
}}
|
||||
>
|
||||
{briefing}
|
||||
</Typography>
|
||||
<p className="briefing-text">{briefing}</p>
|
||||
) : (
|
||||
<>
|
||||
<Skeleton variant="text" sx={{ fontSize: '1rem' }} />
|
||||
|
|
@ -33,4 +24,4 @@ function BriefingWidget({ briefing, sx }) {
|
|||
);
|
||||
}
|
||||
|
||||
export default BriefingWidget;
|
||||
export default BriefingWidget;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Paper, Typography, Box } from '@mui/material';
|
||||
import { Paper, Box } from '@mui/material';
|
||||
|
||||
function GenericWidget({ title, data, children, sx, ...props }) {
|
||||
return (
|
||||
|
|
@ -11,12 +11,12 @@ function GenericWidget({ title, data, children, sx, ...props }) {
|
|||
}}
|
||||
{...props}
|
||||
>
|
||||
<Typography variant="h6" gutterBottom>{title}</Typography>
|
||||
<h2 className="widget-title">{title}</h2>
|
||||
<Box>
|
||||
{children || <Typography>{JSON.stringify(data)}</Typography>}
|
||||
{children || <p>{JSON.stringify(data)}</p>}
|
||||
</Box>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
export default GenericWidget;
|
||||
export default GenericWidget;
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import { Typography, Box, Link, Chip, List, ListItem, ListItemText } from '@mui/material';
|
||||
import { Box, Link, Chip, List, ListItem, ListItemText } from '@mui/material';
|
||||
import { OpenInNew, Comment, TrendingUp } from '@mui/icons-material';
|
||||
import GenericWidget from './GenericWidget';
|
||||
|
||||
function NewsWidget({ data, sx }) {
|
||||
if (!data || data.error) {
|
||||
return (
|
||||
<GenericWidget title="News" sx={{ opacity: 0.5, ...sx }}>
|
||||
<Typography color="text.secondary">
|
||||
<GenericWidget className="news-widget" title="News" sx={{ opacity: 0.5, ...sx }}>
|
||||
<p className="widget-error">
|
||||
{data?.message || 'News data unavailable'}
|
||||
</Typography>
|
||||
</p>
|
||||
</GenericWidget>
|
||||
);
|
||||
}
|
||||
|
|
@ -18,7 +18,7 @@ function NewsWidget({ data, sx }) {
|
|||
const subreddit = data.url?.match(/\/r\/([^\/\.]+)/)?.[1] || 'Reddit';
|
||||
|
||||
return (
|
||||
<GenericWidget title={`r/${subreddit}`} sx={{ ...sx }}>
|
||||
<GenericWidget className="news-widget" title={`r/${subreddit}`} sx={{ ...sx }}>
|
||||
<List dense>
|
||||
{data.posts.slice(0, 5).map((post, index) => (
|
||||
<ListItem key={index} disablePadding sx={{ mb: 1 }}>
|
||||
|
|
@ -55,10 +55,10 @@ function NewsWidget({ data, sx }) {
|
|||
'&:hover': { textDecoration: 'underline' }
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
<div className="news-title">
|
||||
{post.title}
|
||||
<OpenInNew sx={{ fontSize: 14 }} />
|
||||
</Typography>
|
||||
</div>
|
||||
</Link>
|
||||
</Box>
|
||||
}
|
||||
|
|
@ -77,9 +77,7 @@ function NewsWidget({ data, sx }) {
|
|||
size="small"
|
||||
variant="outlined"
|
||||
/>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ ml: 1, alignSelf: 'center' }}>
|
||||
by {post.author}
|
||||
</Typography>
|
||||
<span className="news-author">by {post.author}</span>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
|
|
@ -130,34 +128,23 @@ function NewsWidget({ data, sx }) {
|
|||
'&:hover': { textDecoration: 'underline' }
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
<div className="news-title">
|
||||
{item.title}
|
||||
<OpenInNew sx={{ fontSize: 14 }} />
|
||||
</Typography>
|
||||
</div>
|
||||
</Link>
|
||||
</Box>
|
||||
}
|
||||
secondaryTypographyProps={{ component: 'div' }}
|
||||
secondary={
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
<div className="news-date">
|
||||
{item.pubDate && new Date(item.pubDate).toLocaleDateString()}
|
||||
</Typography>
|
||||
</div>
|
||||
{item.description && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
sx={{
|
||||
display: '-webkit-box',
|
||||
mt: 0.5,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
WebkitLineClamp: 2,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
}}
|
||||
>
|
||||
<div className="news-description">
|
||||
{item.description.replace(/<[^>]*>?/gm, '')}
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
|
|
@ -173,12 +160,12 @@ function NewsWidget({ data, sx }) {
|
|||
if (data.type === 'hackernews' && data.storyIds) {
|
||||
return (
|
||||
<GenericWidget title="Hacker News" sx={{ ...sx }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
<p className="widget-info">
|
||||
{data.storyIds.length} top stories loaded
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
</p>
|
||||
<p className="widget-caption">
|
||||
(Story details will load when API is configured)
|
||||
</Typography>
|
||||
</p>
|
||||
</GenericWidget>
|
||||
);
|
||||
}
|
||||
|
|
@ -186,12 +173,12 @@ function NewsWidget({ data, sx }) {
|
|||
// Default fallback for unknown data types
|
||||
return (
|
||||
<GenericWidget title="News" sx={{ ...sx }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
<p className="widget-info">
|
||||
Data format not recognized
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
</p>
|
||||
<p className="widget-caption">
|
||||
{JSON.stringify(data).substring(0, 100)}...
|
||||
</Typography>
|
||||
</p>
|
||||
</GenericWidget>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Typography, Box, Chip, Stack } from '@mui/material';
|
||||
import { Box, Chip, Stack } from '@mui/material';
|
||||
import { Cloud, WbSunny, Thunderstorm, AcUnit } from '@mui/icons-material';
|
||||
import GenericWidget from './GenericWidget';
|
||||
|
||||
|
|
@ -15,9 +15,9 @@ function WeatherWidget({ data, sx }) {
|
|||
if (!data || data.error) {
|
||||
return (
|
||||
<GenericWidget title="Weather" sx={{ opacity: 0.5, ...sx }}>
|
||||
<Typography color="text.secondary">
|
||||
<p className="widget-error">
|
||||
{data?.message || 'Weather data unavailable'}
|
||||
</Typography>
|
||||
</p>
|
||||
</GenericWidget>
|
||||
);
|
||||
}
|
||||
|
|
@ -39,18 +39,12 @@ function WeatherWidget({ data, sx }) {
|
|||
{getWeatherIcon(description)}
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="h3" component="span">
|
||||
{temp}
|
||||
</Typography>
|
||||
<Typography variant="h5" component="span" color="text.secondary">
|
||||
{tempUnit}
|
||||
</Typography>
|
||||
<span className="weather-temp">{temp}</span>
|
||||
<span className="weather-unit">{tempUnit}</span>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Typography variant="body1" sx={{ mb: 2 }}>
|
||||
{description}
|
||||
</Typography>
|
||||
<p className="weather-description">{description}</p>
|
||||
|
||||
<Stack direction="row" spacing={1}>
|
||||
<Chip
|
||||
|
|
@ -67,18 +61,14 @@ function WeatherWidget({ data, sx }) {
|
|||
|
||||
{data.forecast && data.forecast.length > 0 && (
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
|
||||
Forecast
|
||||
</Typography>
|
||||
<div className="forecast-label">Forecast</div>
|
||||
<Stack direction="row" spacing={1}>
|
||||
{data.forecast.slice(0, 3).map((day, index) => (
|
||||
<Box key={index} sx={{ textAlign: 'center' }}>
|
||||
<Typography variant="caption" display="block">
|
||||
Day {index + 1}
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
<div className="forecast-day">Day {index + 1}</div>
|
||||
<div className="forecast-temp">
|
||||
{day.maxtempF || day.maxtempC}° / {day.mintempF || day.mintempC}°
|
||||
</Typography>
|
||||
</div>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
|
|
|
|||
|
|
@ -1,68 +0,0 @@
|
|||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,6 @@
|
|||
import { createTheme } from '@mui/material/styles';
|
||||
|
||||
const sharedTheme = {
|
||||
typography: {
|
||||
fontFamily: '"Inter", "Roboto", "Helvetica", "Arial", sans-serif',
|
||||
},
|
||||
shape: {
|
||||
borderRadius: 8,
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue