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" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<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>
|
<title>claudash</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,376 @@
|
||||||
#root {
|
/* CSS Reset - replaces MUI CssBaseline */
|
||||||
max-width: 1280px;
|
*,
|
||||||
margin: 0 auto;
|
*::before,
|
||||||
padding: 2rem;
|
*::after {
|
||||||
text-align: center;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
* {
|
||||||
height: 6em;
|
margin: 0;
|
||||||
padding: 1.5em;
|
padding: 0;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes logo-spin {
|
html {
|
||||||
from {
|
-webkit-font-smoothing: antialiased;
|
||||||
transform: rotate(0deg);
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
text-rendering: optimizeLegibility;
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: no-preference) {
|
body {
|
||||||
a:nth-of-type(2) .logo {
|
margin: 0;
|
||||||
animation: logo-spin infinite 20s linear;
|
min-height: 100vh;
|
||||||
}
|
line-height: 1.5;
|
||||||
|
font-synthesis: none;
|
||||||
|
font-weight: 400;
|
||||||
|
font-family: "Chivo", sans-serif !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
h1, h2, h3, h4, h5, h6 {
|
||||||
padding: 2em;
|
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 {
|
.app-light {
|
||||||
color: #888;
|
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 { useState } from 'react';
|
||||||
import Dashboard from './components/Dashboard';
|
import Dashboard from './components/Dashboard';
|
||||||
import DebugPanel from './components/DebugPanel';
|
import DebugPanel from './components/DebugPanel';
|
||||||
import { lightTheme, darkTheme } from './theme';
|
import { lightTheme, darkTheme } from './theme';
|
||||||
|
import './App.css';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [isDark, setIsDark] = useState(true);
|
const [isDark, setIsDark] = useState(true);
|
||||||
|
|
@ -10,14 +11,15 @@ function App() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={isDark ? darkTheme : lightTheme}>
|
<ThemeProvider theme={isDark ? darkTheme : lightTheme}>
|
||||||
<CssBaseline />
|
<div className={isDark ? 'app-dark' : 'app-light'}>
|
||||||
<Dashboard
|
<Dashboard
|
||||||
themeToggle={() => setIsDark(!isDark)}
|
themeToggle={() => setIsDark(!isDark)}
|
||||||
isDark={isDark}
|
isDark={isDark}
|
||||||
/>
|
/>
|
||||||
{debugMode && <DebugPanel />}
|
{debugMode && <DebugPanel />}
|
||||||
|
</div>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState, useRef, useEffect } from 'react';
|
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 { Send, ExpandLess, ExpandMore } from '@mui/icons-material';
|
||||||
import { sendChatMessage } from '../services/claudeService';
|
import { sendChatMessage } from '../services/claudeService';
|
||||||
|
|
||||||
|
|
@ -66,18 +66,16 @@ function ChatInterface() {
|
||||||
>
|
>
|
||||||
{isExpanded ? <ExpandMore /> : <ExpandLess />}
|
{isExpanded ? <ExpandMore /> : <ExpandLess />}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Typography variant="body2" sx={{ ml: 1 }}>
|
<span className="chat-header">Chat with Claude</span>
|
||||||
Chat with Claude
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Messages area - only visible when expanded */}
|
{/* Messages area - only visible when expanded */}
|
||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
<Box sx={{ flex: 1, overflow: 'auto', p: 2 }}>
|
<Box sx={{ flex: 1, overflow: 'auto', p: 2 }}>
|
||||||
{messages.length === 0 && (
|
{messages.length === 0 && (
|
||||||
<Typography color="text.secondary" variant="body2">
|
<p className="chat-placeholder">
|
||||||
Ask me anything about your briefing or the news...
|
Ask me anything about your briefing or the news...
|
||||||
</Typography>
|
</p>
|
||||||
)}
|
)}
|
||||||
{messages.map((msg, idx) => (
|
{messages.map((msg, idx) => (
|
||||||
<Box
|
<Box
|
||||||
|
|
@ -87,19 +85,9 @@ function ChatInterface() {
|
||||||
textAlign: msg.role === 'user' ? 'right' : 'left'
|
textAlign: msg.role === 'user' ? 'right' : 'left'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography
|
<div className={msg.role === 'user' ? 'chat-message-user' : 'chat-message-assistant'}>
|
||||||
variant="body2"
|
|
||||||
sx={{
|
|
||||||
display: 'inline-block',
|
|
||||||
bgcolor: msg.role === 'user' ? 'primary.main' : 'grey.800',
|
|
||||||
color: 'white',
|
|
||||||
p: 1.5,
|
|
||||||
borderRadius: 2,
|
|
||||||
maxWidth: '70%'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{msg.content}
|
{msg.content}
|
||||||
</Typography>
|
</div>
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
<div ref={messagesEndRef} />
|
<div ref={messagesEndRef} />
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState, useEffect } from 'react';
|
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 { Brightness4, Brightness7, Refresh } from '@mui/icons-material';
|
||||||
import { fetchAllSources } from '../services/dataFetcher';
|
import { fetchAllSources } from '../services/dataFetcher';
|
||||||
import { generateBriefing } from '../services/claudeService';
|
import { generateBriefing } from '../services/claudeService';
|
||||||
|
|
@ -10,241 +10,233 @@ import NewsWidget from './widgets/NewsWidget';
|
||||||
import ChatInterface from './ChatInterface';
|
import ChatInterface from './ChatInterface';
|
||||||
|
|
||||||
function Dashboard({ themeToggle, isDark }) {
|
function Dashboard({ themeToggle, isDark }) {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [loadingStatus, setLoadingStatus] = useState('Initializing...');
|
const [loadingStatus, setLoadingStatus] = useState('Initializing...');
|
||||||
const [briefing, setBriefing] = useState('');
|
const [briefing, setBriefing] = useState('');
|
||||||
const [sourceData, setSourceData] = useState({});
|
const [sourceData, setSourceData] = useState({});
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
const [tokenUsage, setTokenUsage] = useState(null);
|
const [tokenUsage, setTokenUsage] = useState(null);
|
||||||
const [cacheInfo, setCacheInfo] = useState(null);
|
const [cacheInfo, setCacheInfo] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadDashboard();
|
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...');
|
|
||||||
|
|
||||||
|
async function loadDashboard(forceRefresh = false) {
|
||||||
try {
|
try {
|
||||||
// Generate today's briefing with Claude
|
console.log('=== loadDashboard called ===');
|
||||||
const result = await generateBriefing(data, previousBriefing);
|
console.log('Force refresh:', forceRefresh);
|
||||||
console.log('Briefing generated');
|
setError(null);
|
||||||
|
|
||||||
// Handle result object with text and usage
|
// Check if we already have today's briefing (unless forcing refresh)
|
||||||
if (result && typeof result === 'object' && result.text) {
|
if (!forceRefresh && hasTodaysBriefing()) {
|
||||||
setBriefing(result.text);
|
console.log('Using cached briefing from today');
|
||||||
setTokenUsage(result.usage);
|
const todaysBriefing = getTodaysBriefing();
|
||||||
setCacheInfo(null); // Clear cache info when generating fresh briefing
|
setBriefing(todaysBriefing.briefing);
|
||||||
console.log('Token usage:', result.usage);
|
setSourceData(todaysBriefing.sources || {});
|
||||||
} else if (typeof result === 'string') {
|
|
||||||
// Fallback for string responses
|
|
||||||
setBriefing(result);
|
|
||||||
setCacheInfo(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save to localStorage
|
// Calculate cache age
|
||||||
const today = new Date().toISOString().split('T')[0];
|
const cacheAge = Date.now() - todaysBriefing.timestamp;
|
||||||
console.log('Saving briefing for:', today);
|
const cacheAgeMinutes = Math.floor(cacheAge / (1000 * 60));
|
||||||
const briefingText = result?.text || result;
|
|
||||||
saveBriefing(today, briefingText, data.sources);
|
// Set cache info to display message
|
||||||
} catch (briefingError) {
|
setCacheInfo({
|
||||||
console.error('Failed to generate briefing:', briefingError);
|
age: cacheAgeMinutes,
|
||||||
setBriefing(`Unable to generate AI briefing: ${briefingError.message}\n\nYou can still view the data widgets below.`);
|
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 = () => {
|
const handleRefresh = () => {
|
||||||
setRefreshing(true);
|
setRefreshing(true);
|
||||||
loadDashboard(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 (
|
return (
|
||||||
<Box display="flex" flexDirection="column" justifyContent="center" alignItems="center" minHeight="100vh" gap={2}>
|
<Box sx={{ minHeight: '100vh', pb: 10 }}>
|
||||||
<CircularProgress />
|
{/* Top toolbar */}
|
||||||
<Typography variant="body1" color="text.secondary">
|
<Box sx={{
|
||||||
{loadingStatus}
|
position: 'fixed',
|
||||||
</Typography>
|
top: 0,
|
||||||
</Box>
|
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 (
|
<Container maxWidth="xl" sx={{ pt: 4 }}>
|
||||||
<Box sx={{ minHeight: '100vh', pb: 10 }}>
|
{error && (
|
||||||
{/* Top toolbar */}
|
<Alert severity="error" sx={{ mb: 2 }}>
|
||||||
<Box sx={{
|
{error}
|
||||||
position: 'fixed',
|
</Alert>
|
||||||
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 }}>
|
{tokenUsage && (
|
||||||
{error && (
|
<div className="token-usage-box">
|
||||||
<Alert severity="error" sx={{ mb: 2 }}>
|
<p className="token-usage-text">
|
||||||
{error}
|
<strong>API Usage:</strong> {tokenUsage.input_tokens?.toLocaleString()} input + {tokenUsage.output_tokens?.toLocaleString()} output = {(tokenUsage.input_tokens + tokenUsage.output_tokens).toLocaleString()} total tokens
|
||||||
</Alert>
|
{' • '}
|
||||||
)}
|
<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 && (
|
{cacheInfo && (
|
||||||
<Box sx={{ mb: 2, p: 2, bgcolor: 'background.paper', borderRadius: 1, border: '1px solid', borderColor: 'divider' }}>
|
<div className="cache-info-box">
|
||||||
<Typography variant="body2" color="text.secondary">
|
<p className="cache-info-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>💾 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
|
||||||
{' • '}
|
{' '}
|
||||||
<strong>Cost:</strong> ${((tokenUsage.input_tokens * 0.003 / 1000) + (tokenUsage.output_tokens * 0.015 / 1000)).toFixed(4)}
|
<span className="cache-info-caption">
|
||||||
{' '}
|
(Click refresh to generate a new briefing)
|
||||||
<Typography component="span" variant="caption" color="text.disabled">
|
</span>
|
||||||
(Claude Sonnet 4 pricing: $0.003/1K input, $0.015/1K output)
|
</p>
|
||||||
</Typography>
|
</div>
|
||||||
</Typography>
|
)}
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{cacheInfo && (
|
<Box className="widget-grid">
|
||||||
<Box sx={{ mb: 2, p: 2, bgcolor: 'success.main', color: 'success.contrastText', borderRadius: 1, border: '1px solid', borderColor: 'success.dark' }}>
|
<Box className="widget-row">
|
||||||
<Typography variant="body2">
|
<BriefingWidget briefing={briefing} />
|
||||||
<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
|
<WeatherWidget
|
||||||
{' '}
|
data={sourceData.weather}
|
||||||
<Typography component="span" variant="caption" sx={{ opacity: 0.9 }}>
|
/>
|
||||||
(Click refresh to generate a new briefing)
|
</Box>
|
||||||
</Typography>
|
<Box className="widget-row condensed">
|
||||||
</Typography>
|
{/* Render NewsWidget for each news source */}
|
||||||
</Box>
|
{Object.entries(sourceData).map(([key, data]) => {
|
||||||
)}
|
if (key === 'weather') return null;
|
||||||
|
return (
|
||||||
|
<NewsWidget
|
||||||
|
key={key}
|
||||||
|
data={data}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
|
||||||
{/* Widget Grid - easily rearrangeable */}
|
{/* Fixed chat at bottom */}
|
||||||
<Box sx={{
|
<ChatInterface />
|
||||||
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' }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Box>
|
</Box>
|
||||||
</Container>
|
);
|
||||||
|
|
||||||
{/* Fixed chat at bottom */}
|
|
||||||
<ChatInterface />
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Dashboard;
|
export default Dashboard;
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState } from 'react';
|
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 { Download, Upload } from '@mui/icons-material';
|
||||||
import { exportAllData, importDummyData } from '../services/storageService';
|
import { exportAllData, importDummyData } from '../services/storageService';
|
||||||
|
|
||||||
|
|
@ -38,7 +38,7 @@ function DebugPanel() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper sx={{ position: 'fixed', bottom: 80, right: 16, p: 2, width: 250, zIndex: 999 }}>
|
<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
|
<Button
|
||||||
fullWidth
|
fullWidth
|
||||||
|
|
@ -66,13 +66,7 @@ function DebugPanel() {
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{dummyMode && (
|
{dummyMode && (
|
||||||
<Typography
|
<span className="debug-warning">Using dummy data</span>
|
||||||
variant="caption"
|
|
||||||
color="warning.main"
|
|
||||||
sx={{ mt: 1, display: 'block' }}
|
|
||||||
>
|
|
||||||
Using dummy data
|
|
||||||
</Typography>
|
|
||||||
)}
|
)}
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Typography, Skeleton, Box } from '@mui/material';
|
import { Skeleton, Box } from '@mui/material';
|
||||||
import GenericWidget from './GenericWidget';
|
import GenericWidget from './GenericWidget';
|
||||||
|
|
||||||
function BriefingWidget({ briefing, sx }) {
|
function BriefingWidget({ briefing, sx }) {
|
||||||
|
|
@ -9,16 +9,7 @@ function BriefingWidget({ briefing, sx }) {
|
||||||
>
|
>
|
||||||
<Box sx={{ mt: 1 }}>
|
<Box sx={{ mt: 1 }}>
|
||||||
{briefing ? (
|
{briefing ? (
|
||||||
<Typography
|
<p className="briefing-text">{briefing}</p>
|
||||||
variant="body1"
|
|
||||||
sx={{
|
|
||||||
whiteSpace: 'pre-line',
|
|
||||||
lineHeight: 1.7,
|
|
||||||
fontSize: '1.05rem'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{briefing}
|
|
||||||
</Typography>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Skeleton variant="text" sx={{ fontSize: '1rem' }} />
|
<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 }) {
|
function GenericWidget({ title, data, children, sx, ...props }) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -11,12 +11,12 @@ function GenericWidget({ title, data, children, sx, ...props }) {
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<Typography variant="h6" gutterBottom>{title}</Typography>
|
<h2 className="widget-title">{title}</h2>
|
||||||
<Box>
|
<Box>
|
||||||
{children || <Typography>{JSON.stringify(data)}</Typography>}
|
{children || <p>{JSON.stringify(data)}</p>}
|
||||||
</Box>
|
</Box>
|
||||||
</Paper>
|
</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 { OpenInNew, Comment, TrendingUp } from '@mui/icons-material';
|
||||||
import GenericWidget from './GenericWidget';
|
import GenericWidget from './GenericWidget';
|
||||||
|
|
||||||
function NewsWidget({ data, sx }) {
|
function NewsWidget({ data, sx }) {
|
||||||
if (!data || data.error) {
|
if (!data || data.error) {
|
||||||
return (
|
return (
|
||||||
<GenericWidget title="News" sx={{ opacity: 0.5, ...sx }}>
|
<GenericWidget className="news-widget" title="News" sx={{ opacity: 0.5, ...sx }}>
|
||||||
<Typography color="text.secondary">
|
<p className="widget-error">
|
||||||
{data?.message || 'News data unavailable'}
|
{data?.message || 'News data unavailable'}
|
||||||
</Typography>
|
</p>
|
||||||
</GenericWidget>
|
</GenericWidget>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -18,7 +18,7 @@ function NewsWidget({ data, sx }) {
|
||||||
const subreddit = data.url?.match(/\/r\/([^\/\.]+)/)?.[1] || 'Reddit';
|
const subreddit = data.url?.match(/\/r\/([^\/\.]+)/)?.[1] || 'Reddit';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GenericWidget title={`r/${subreddit}`} sx={{ ...sx }}>
|
<GenericWidget className="news-widget" title={`r/${subreddit}`} sx={{ ...sx }}>
|
||||||
<List dense>
|
<List dense>
|
||||||
{data.posts.slice(0, 5).map((post, index) => (
|
{data.posts.slice(0, 5).map((post, index) => (
|
||||||
<ListItem key={index} disablePadding sx={{ mb: 1 }}>
|
<ListItem key={index} disablePadding sx={{ mb: 1 }}>
|
||||||
|
|
@ -55,10 +55,10 @@ function NewsWidget({ data, sx }) {
|
||||||
'&:hover': { textDecoration: 'underline' }
|
'&:hover': { textDecoration: 'underline' }
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="body2" sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
<div className="news-title">
|
||||||
{post.title}
|
{post.title}
|
||||||
<OpenInNew sx={{ fontSize: 14 }} />
|
<OpenInNew sx={{ fontSize: 14 }} />
|
||||||
</Typography>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
|
|
@ -77,9 +77,7 @@ function NewsWidget({ data, sx }) {
|
||||||
size="small"
|
size="small"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
/>
|
/>
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ ml: 1, alignSelf: 'center' }}>
|
<span className="news-author">by {post.author}</span>
|
||||||
by {post.author}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
@ -130,34 +128,23 @@ function NewsWidget({ data, sx }) {
|
||||||
'&:hover': { textDecoration: 'underline' }
|
'&:hover': { textDecoration: 'underline' }
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="body2" sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
<div className="news-title">
|
||||||
{item.title}
|
{item.title}
|
||||||
<OpenInNew sx={{ fontSize: 14 }} />
|
<OpenInNew sx={{ fontSize: 14 }} />
|
||||||
</Typography>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
secondaryTypographyProps={{ component: 'div' }}
|
secondaryTypographyProps={{ component: 'div' }}
|
||||||
secondary={
|
secondary={
|
||||||
<Box>
|
<Box>
|
||||||
<Typography variant="caption" color="text.secondary">
|
<div className="news-date">
|
||||||
{item.pubDate && new Date(item.pubDate).toLocaleDateString()}
|
{item.pubDate && new Date(item.pubDate).toLocaleDateString()}
|
||||||
</Typography>
|
</div>
|
||||||
{item.description && (
|
{item.description && (
|
||||||
<Typography
|
<div className="news-description">
|
||||||
variant="caption"
|
|
||||||
color="text.secondary"
|
|
||||||
sx={{
|
|
||||||
display: '-webkit-box',
|
|
||||||
mt: 0.5,
|
|
||||||
overflow: 'hidden',
|
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
WebkitLineClamp: 2,
|
|
||||||
WebkitBoxOrient: 'vertical',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{item.description.replace(/<[^>]*>?/gm, '')}
|
{item.description.replace(/<[^>]*>?/gm, '')}
|
||||||
</Typography>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
|
|
@ -173,12 +160,12 @@ function NewsWidget({ data, sx }) {
|
||||||
if (data.type === 'hackernews' && data.storyIds) {
|
if (data.type === 'hackernews' && data.storyIds) {
|
||||||
return (
|
return (
|
||||||
<GenericWidget title="Hacker News" sx={{ ...sx }}>
|
<GenericWidget title="Hacker News" sx={{ ...sx }}>
|
||||||
<Typography variant="body2" color="text.secondary">
|
<p className="widget-info">
|
||||||
{data.storyIds.length} top stories loaded
|
{data.storyIds.length} top stories loaded
|
||||||
</Typography>
|
</p>
|
||||||
<Typography variant="caption" color="text.secondary">
|
<p className="widget-caption">
|
||||||
(Story details will load when API is configured)
|
(Story details will load when API is configured)
|
||||||
</Typography>
|
</p>
|
||||||
</GenericWidget>
|
</GenericWidget>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -186,12 +173,12 @@ function NewsWidget({ data, sx }) {
|
||||||
// Default fallback for unknown data types
|
// Default fallback for unknown data types
|
||||||
return (
|
return (
|
||||||
<GenericWidget title="News" sx={{ ...sx }}>
|
<GenericWidget title="News" sx={{ ...sx }}>
|
||||||
<Typography variant="body2" color="text.secondary">
|
<p className="widget-info">
|
||||||
Data format not recognized
|
Data format not recognized
|
||||||
</Typography>
|
</p>
|
||||||
<Typography variant="caption" color="text.secondary">
|
<p className="widget-caption">
|
||||||
{JSON.stringify(data).substring(0, 100)}...
|
{JSON.stringify(data).substring(0, 100)}...
|
||||||
</Typography>
|
</p>
|
||||||
</GenericWidget>
|
</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 { Cloud, WbSunny, Thunderstorm, AcUnit } from '@mui/icons-material';
|
||||||
import GenericWidget from './GenericWidget';
|
import GenericWidget from './GenericWidget';
|
||||||
|
|
||||||
|
|
@ -15,9 +15,9 @@ function WeatherWidget({ data, sx }) {
|
||||||
if (!data || data.error) {
|
if (!data || data.error) {
|
||||||
return (
|
return (
|
||||||
<GenericWidget title="Weather" sx={{ opacity: 0.5, ...sx }}>
|
<GenericWidget title="Weather" sx={{ opacity: 0.5, ...sx }}>
|
||||||
<Typography color="text.secondary">
|
<p className="widget-error">
|
||||||
{data?.message || 'Weather data unavailable'}
|
{data?.message || 'Weather data unavailable'}
|
||||||
</Typography>
|
</p>
|
||||||
</GenericWidget>
|
</GenericWidget>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -39,18 +39,12 @@ function WeatherWidget({ data, sx }) {
|
||||||
{getWeatherIcon(description)}
|
{getWeatherIcon(description)}
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<Typography variant="h3" component="span">
|
<span className="weather-temp">{temp}</span>
|
||||||
{temp}
|
<span className="weather-unit">{tempUnit}</span>
|
||||||
</Typography>
|
|
||||||
<Typography variant="h5" component="span" color="text.secondary">
|
|
||||||
{tempUnit}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Typography variant="body1" sx={{ mb: 2 }}>
|
<p className="weather-description">{description}</p>
|
||||||
{description}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Stack direction="row" spacing={1}>
|
<Stack direction="row" spacing={1}>
|
||||||
<Chip
|
<Chip
|
||||||
|
|
@ -67,18 +61,14 @@ function WeatherWidget({ data, sx }) {
|
||||||
|
|
||||||
{data.forecast && data.forecast.length > 0 && (
|
{data.forecast && data.forecast.length > 0 && (
|
||||||
<Box sx={{ mt: 2 }}>
|
<Box sx={{ mt: 2 }}>
|
||||||
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
|
<div className="forecast-label">Forecast</div>
|
||||||
Forecast
|
|
||||||
</Typography>
|
|
||||||
<Stack direction="row" spacing={1}>
|
<Stack direction="row" spacing={1}>
|
||||||
{data.forecast.slice(0, 3).map((day, index) => (
|
{data.forecast.slice(0, 3).map((day, index) => (
|
||||||
<Box key={index} sx={{ textAlign: 'center' }}>
|
<Box key={index} sx={{ textAlign: 'center' }}>
|
||||||
<Typography variant="caption" display="block">
|
<div className="forecast-day">Day {index + 1}</div>
|
||||||
Day {index + 1}
|
<div className="forecast-temp">
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2">
|
|
||||||
{day.maxtempF || day.maxtempC}° / {day.mintempF || day.mintempC}°
|
{day.maxtempF || day.maxtempC}° / {day.mintempF || day.mintempC}°
|
||||||
</Typography>
|
</div>
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</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';
|
import { createTheme } from '@mui/material/styles';
|
||||||
|
|
||||||
const sharedTheme = {
|
const sharedTheme = {
|
||||||
typography: {
|
|
||||||
fontFamily: '"Inter", "Roboto", "Helvetica", "Arial", sans-serif',
|
|
||||||
},
|
|
||||||
shape: {
|
shape: {
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue