diff --git a/favicon/android-chrome-192x192.png b/favicon/android-chrome-192x192.png new file mode 100644 index 0000000..e00a622 Binary files /dev/null and b/favicon/android-chrome-192x192.png differ diff --git a/favicon/android-chrome-512x512.png b/favicon/android-chrome-512x512.png new file mode 100644 index 0000000..a183d43 Binary files /dev/null and b/favicon/android-chrome-512x512.png differ diff --git a/favicon/apple-touch-icon.png b/favicon/apple-touch-icon.png new file mode 100644 index 0000000..266dd23 Binary files /dev/null and b/favicon/apple-touch-icon.png differ diff --git a/favicon/favicon-16x16.png b/favicon/favicon-16x16.png new file mode 100644 index 0000000..2d9919a Binary files /dev/null and b/favicon/favicon-16x16.png differ diff --git a/favicon/favicon-32x32.png b/favicon/favicon-32x32.png new file mode 100644 index 0000000..06746cc Binary files /dev/null and b/favicon/favicon-32x32.png differ diff --git a/favicon/favicon.ico b/favicon/favicon.ico new file mode 100644 index 0000000..49528b2 Binary files /dev/null and b/favicon/favicon.ico differ diff --git a/index.html b/index.html index 60a55af..b008559 100644 --- a/index.html +++ b/index.html @@ -1,15 +1,24 @@ - - - - React Verse - Free API Dashboard - - - - - -
- - - + + + + + React Verse - Free API Dashboard + + + + + + + + + + + + +
+ + + + \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx index 14d4735..d41c81b 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -41,19 +41,30 @@ import JokesQuotes from './pages/JokesQuotes.jsx'; import Pets from './pages/Pets.jsx'; import Covid from './pages/Covid.jsx'; import Navbar from './components/Navbar.jsx'; -import ThemeSwitcher from './components/ThemeSwitcher.jsx'; // TODO: Extract theme state into context (see todo 5). -import { useState } from 'react'; +import { useState, useEffect } from 'react'; export default function App() { - const [theme, setTheme] = useState('light'); - const toggleTheme = () => setTheme(t => (t === 'light' ? 'dark' : 'light')); + const [theme, setTheme] = useState(() => { + return localStorage.getItem('theme') || 'light'; + }); + + const toggleTheme = () => { + setTheme(t => { + const newTheme = t === 'light' ? 'dark' : 'light'; + localStorage.setItem('theme', newTheme); + return newTheme; + }); + }; + + useEffect(() => { + localStorage.setItem('theme', theme); + }, [theme]); return (
- - +
{/* Different Routes */} diff --git a/src/components/Navbar.jsx b/src/components/Navbar.jsx index b730c14..55ec1ad 100644 --- a/src/components/Navbar.jsx +++ b/src/components/Navbar.jsx @@ -1,12 +1,10 @@ import { NavLink } from 'react-router-dom'; -export default function Navbar() { +export default function Navbar({ theme, toggleTheme }) { return ( ); } diff --git a/src/components/ThemeSwitcher.jsx b/src/components/ThemeSwitcher.jsx deleted file mode 100644 index 281cd7e..0000000 --- a/src/components/ThemeSwitcher.jsx +++ /dev/null @@ -1,7 +0,0 @@ -export default function ThemeSwitcher({ theme, toggleTheme }) { - return ( -
- -
- ); -} diff --git a/src/styles.css b/src/styles.css index 1bcbd72..488079f 100644 --- a/src/styles.css +++ b/src/styles.css @@ -9,6 +9,7 @@ --radius: 8px; --transition: 0.25s ease; } + .theme-dark, [data-theme="dark"], .theme-dark :root { @@ -27,16 +28,67 @@ body, font-family: system-ui, Arial, sans-serif; margin: 0; min-height: 100vh; + overflow-x: hidden; +} + +.app { + transition: background 0.3s ease, color 0.3s ease; +} + +/* Full page slide animation */ +main { + animation: fullPageSlide 0.6s ease-out; +} + +@keyframes fullPageSlide { + from { + opacity: 0; + transform: translateX(100px); + } + to { + opacity: 1; + transform: translateX(0); + } } .container { padding: 1rem clamp(1rem, 2vw, 2rem); + max-width: 100%; + box-sizing: border-box; +} + +@keyframes pageSlideIn { + from { + opacity: 0; + transform: translateX(-30px) translateY(20px); + } + to { + opacity: 1; + transform: translateX(0) translateY(0); + } +} + +@keyframes cardSlideIn { + from { + opacity: 0; + transform: translateY(40px) scale(0.95); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +/* Page transition effect */ +.page-transition { + animation: pageSlideIn 0.5s ease-out; } a { color: var(--primary); text-decoration: none; } + a:hover { text-decoration: underline; } @@ -53,6 +105,7 @@ a:hover { top: 0; z-index: 10; } + .navbar .logo { margin: 0 5vw; font-size: 1.85rem; @@ -67,7 +120,7 @@ a:hover { .theme-dark .navbar .logo, [data-theme="dark"] .navbar .logo { background: linear-gradient(90deg, #00d9ff, #b0e7ef); - -webkit-background-clip: text; + -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; text-fill-color: transparent; @@ -81,17 +134,28 @@ a:hover { gap: 0.95rem; margin: 0 8vw; padding: 0; + align-items: center; } .navbar a { text-decoration: none; color: #1a5c66; font-weight: 700; - padding: 0.5rem; + padding: 0.5rem; + border-radius: 4px; + position: relative; + transition: all 0.3s ease; + transform: translateY(0); } -.navbar a:hover{ - background: rgba(0, 217, 255,0.1); +.navbar a:hover { + background: rgba(0, 217, 255, 0.1); + transform: translateY(-2px); +} + +.navbar a:active { + transform: translateY(0) scale(0.95); + transition: transform 0.1s ease; } .theme-dark .navbar a, @@ -101,30 +165,94 @@ a:hover { .navbar a.active { font-weight: 600; + background: rgba(0, 217, 255, 0.15); + color: #0f5a66; +} + +.theme-dark .navbar a.active, +[data-theme="dark"] .navbar a.active { + background: rgba(0, 217, 255, 0.2); + color: #00d9ff; +} + +.theme-item { + margin-left: auto; +} + +.theme-toggle { + background: linear-gradient(45deg, rgb(0, 166, 255), rgb(7, 94, 105)); + color: white; + border: none; + padding: 0.5rem 0.75rem; + border-radius: 6px; + font-size: 0.9rem; + font-weight: 500; + cursor: pointer; + transition: var(--transition); + display: flex; + align-items: center; + gap: 0.5rem; +} + +.theme-toggle:hover { + background: #215b6a; +} + +.theme-text { + display: none; } + +.theme-icon { + font-size: 1.1rem; +} + .nav-toggle { display: none; + margin-left: auto; + background: none; + border: 1px solid var(--border); + color: var(--text); + padding: 0.5rem 0.75rem; + border-radius: var(--radius); + cursor: pointer; } + @media (max-width: 900px) { + .navbar { + flex-wrap: wrap; + position: relative; + } + .navbar ul { flex-direction: column; position: absolute; left: 0; right: 0; - top: 56px; + top: 100%; background: var(--bg-alt); padding: 1rem; display: none; + border-top: 1px solid var(--border); + width: 100%; + box-sizing: border-box; + z-index: 1000; + } + + .navbar ul .theme-item { + margin-left: 0; + margin-top: 0.5rem; } + + .theme-text { + display: inline; + } + body.nav-open .navbar ul { display: flex; } + .nav-toggle { display: block; - background: none; - border: 1px solid var(--border); - padding: 0.5rem 0.75rem; - border-radius: var(--radius); } } @@ -135,15 +263,19 @@ a:hover { grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); margin-top: 1rem; } + .flex { display: flex; } + .gap { gap: 1rem; } + .wrap { flex-wrap: wrap; } + .inline-form { display: flex; gap: 0.5rem; @@ -162,9 +294,9 @@ a:hover { box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08); } -.home-page .card{ - background: rgba(26, 92, 102,0.2); - border: 2px solid rgba(0, 217, 255,0.5); +.home-page .card { + background: rgba(26, 92, 102, 0.2); + border: 2px solid rgba(0, 217, 255, 0.5); } .home-page .card:hover h3 { @@ -177,6 +309,7 @@ a:hover { font-size: 1rem; transition: transform 1.5s ease; } + .card-footer { margin-top: auto; font-size: 0.85rem; @@ -185,7 +318,7 @@ a:hover { .card-link-button { display: inline-block; - background:linear-gradient(45deg, rgb(0, 166, 255),rgb(7, 94, 105)); + background: linear-gradient(45deg, rgb(0, 166, 255), rgb(7, 94, 105)); color: white; padding: 0.5rem 0.75rem; border-radius: 6px; @@ -196,18 +329,16 @@ a:hover { } .card-link-button:hover { - background: #215b6a; + background: #215b6a; text-decoration: none; } .chart-placeholder { - background: repeating-linear-gradient( - 45deg, - var(--bg), - var(--bg) 10px, - var(--bg-alt) 10px, - var(--bg-alt) 20px - ); + background: repeating-linear-gradient(45deg, + var(--bg), + var(--bg) 10px, + var(--bg-alt) 10px, + var(--bg-alt) 20px); border: 1px dashed var(--border); padding: 2rem; text-align: center; @@ -237,6 +368,7 @@ select { background: var(--bg); color: var(--text); } + button { cursor: pointer; background: var(--primary); @@ -244,13 +376,16 @@ button { border-color: var(--primary); transition: var(--transition); } + button:hover { filter: brightness(1.1); } + button:disabled { opacity: 0.5; cursor: not-allowed; } + input, select { background: var(--bg-alt); @@ -261,14 +396,17 @@ select { padding: 0.5rem 0; opacity: 0.8; } + .error { color: var(--danger); font-weight: 600; } + .correct { background: #16a34a !important; color: #fff; } + .wrong { background: #dc2626 !important; color: #fff; @@ -285,6 +423,7 @@ img { right: 1rem; z-index: 100; } + .theme-switcher button { background: var(--bg-alt); color: var(--text); @@ -309,10 +448,12 @@ blockquote { vertical-align: middle; margin-left: 5px; } + @keyframes skeleton-loading { 0% { background-color: hsl(200, 20%, 70%); } + 100% { background-color: hsl(180, 2%, 88%); } @@ -334,7 +475,8 @@ blockquote { opacity: 1; pointer-events: none; } -.bg-layer + .bg-layer { + +.bg-layer+.bg-layer { opacity: 0; } @@ -342,21 +484,27 @@ blockquote { .weather-bg-sunny { background: linear-gradient(135deg, #e6f7ff 0%, #cdeeff 50%, #a6ddff 100%) !important; } + .weather-bg-rain { background: linear-gradient(135deg, #bdc3c7 0%, #2c3e50 50%, #34495e 100%) !important; } + .weather-bg-cloud { background: linear-gradient(135deg, #ece9e6 0%, #ffffff 50%, #f8f9fa 100%) !important; } + .weather-bg-snow { background: linear-gradient(135deg, #e0eafc 0%, #cfdef3 50%, #a8c0ff 100%) !important; } + .weather-bg-fog { background: linear-gradient(135deg, #bdc3c7 0%, #757f9a 50%, #5c6b8a 100%) !important; } + .weather-bg-storm { background: linear-gradient(135deg, #2c3e50 0%, #34495e 50%, #1a252f 100%) !important; } + .weather-bg-default { background: linear-gradient(135deg, #89f7fe 0%, #66a6ff 50%, #4facfe 100%) !important; } @@ -373,6 +521,7 @@ blockquote { align-items: center; justify-content: center; } + .sun { width: 72px; height: 72px; @@ -382,36 +531,82 @@ blockquote { 0 0 80px 36px rgba(255, 183, 77, 0.06); animation: sun-pulse 6s ease-in-out infinite, sun-bob 10s ease-in-out infinite; } + @keyframes sun-pulse { - 0%, 100% { transform: scale(1); opacity: 0.98; } - 50% { transform: scale(1.05); opacity: 1; } + + 0%, + 100% { + transform: scale(1); + opacity: 0.98; + } + + 50% { + transform: scale(1.05); + opacity: 1; + } } + @keyframes sun-bob { - 0%, 100% { transform: translateY(0); } - 50% { transform: translateY(-6px); } + + 0%, + 100% { + transform: translateY(0); + } + + 50% { + transform: translateY(-6px); + } } /* Clouds */ -.cloud-svg { position: absolute; pointer-events: none; opacity: 1; filter: drop-shadow(0 10px 18px rgba(0,0,0,0.1)); } -.cloud-svg .cloud-shape { fill: #fff; stroke: rgba(0,0,0,0.09); stroke-width: 0.9; } +.cloud-svg { + position: absolute; + pointer-events: none; + opacity: 1; + filter: drop-shadow(0 10px 18px rgba(0, 0, 0, 0.1)); +} + +.cloud-svg .cloud-shape { + fill: #fff; + stroke: rgba(0, 0, 0, 0.09); + stroke-width: 0.9; +} + @keyframes cloud-drift { - from { transform: translateX(0); } - to { transform: translateX(18vw); } + from { + transform: translateX(0); + } + + to { + transform: translateX(18vw); + } } /* Rain */ -.rain-layer { position: absolute; inset: 10% 0 0 0; height: 80%; overflow: visible; } +.rain-layer { + position: absolute; + inset: 10% 0 0 0; + height: 80%; + overflow: visible; +} + .raindrop { position: absolute; top: -8%; width: 2px; height: 28vh; - background: linear-gradient(180deg, rgba(255,255,255,0.2), rgba(255,255,255,0.02)); + background: linear-gradient(180deg, rgba(255, 255, 255, 0.2), rgba(255, 255, 255, 0.02)); opacity: 0.7; border-radius: 2px; animation: fall 1.1s linear infinite; } -@keyframes fall { to { transform: translateY(110vh); opacity: 0.2; } } + +@keyframes fall { + to { + transform: translateY(110vh); + opacity: 0.2; + } +} /* Snow */ .snowflake { @@ -419,13 +614,20 @@ blockquote { top: -10%; width: var(--size, 8px); height: var(--size, 8px); - background: rgba(255,255,255,0.95); + background: rgba(255, 255, 255, 0.95); border-radius: 50%; animation: snow-fall var(--dur, 8s) linear infinite; } + @keyframes snow-fall { - 0% { transform: translateY(-10vh) translateX(0); } - 100% { transform: translateY(110vh) translateX(var(--drift)); opacity: 0.7; } + 0% { + transform: translateY(-10vh) translateX(0); + } + + 100% { + transform: translateY(110vh) translateX(var(--drift)); + opacity: 0.7; + } } /* Fog */ @@ -434,28 +636,55 @@ blockquote { left: 0; right: 0; height: 10%; - background: linear-gradient(180deg, rgba(255,255,255,0.08), rgba(255,255,255,0.02)); + background: linear-gradient(180deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.02)); filter: blur(6px); opacity: 0.4; } -.fog--one { top: 20%; animation: fog-move 18s ease-in-out infinite; } -.fog--two { top: 48%; animation: fog-move 26s ease-in-out infinite reverse; } + +.fog--one { + top: 20%; + animation: fog-move 18s ease-in-out infinite; +} + +.fog--two { + top: 48%; + animation: fog-move 26s ease-in-out infinite reverse; +} + @keyframes fog-move { - 0% { transform: translateX(-5vw); } - 50% { transform: translateX(6vw); } - 100% { transform: translateX(-5vw); } + 0% { + transform: translateX(-5vw); + } + + 50% { + transform: translateX(6vw); + } + + 100% { + transform: translateX(-5vw); + } } /* Storm */ .lightning { position: absolute; width: 2px; - background: linear-gradient(180deg, rgba(255,255,255,0.9), rgba(255,255,255,0.1)); + background: linear-gradient(180deg, rgba(255, 255, 255, 0.9), rgba(255, 255, 255, 0.1)); opacity: 0; } + @keyframes lightning-flash { - 0%, 85%, 100% { opacity: 0; } - 5%, 8% { opacity: 1; } + + 0%, + 85%, + 100% { + opacity: 0; + } + + 5%, + 8% { + opacity: 1; + } } /* Dev tools styling */ @@ -465,18 +694,20 @@ blockquote { gap: 0.5rem; flex-wrap: wrap; } + .dev-btn { font-size: 0.8rem; padding: 0.5rem 0.75rem; - background: rgba(255,255,255,0.4); - border: 1px solid rgba(255,255,255,0.2); + background: rgba(255, 255, 255, 0.4); + border: 1px solid rgba(255, 255, 255, 0.2); border-radius: 8px; color: var(--text); cursor: pointer; transition: all 0.3s ease; } + .dev-btn:hover { - background: rgba(255,255,255,0.2); + background: rgba(255, 255, 255, 0.2); transform: translateY(-1px); } @@ -487,8 +718,11 @@ blockquote { padding: 2rem clamp(1rem, 4vw, 3rem); box-sizing: border-box; } + @media (max-width: 640px) { - .weather-inner { padding: 1rem; } + .weather-inner { + padding: 1rem; + } } @@ -497,10 +731,10 @@ blockquote { .pagination { width: 100%; display: flex; - justify-content: center; - align-items: center; - margin-top: 20px; - gap: 15px; + justify-content: center; + align-items: center; + margin-top: 20px; + gap: 15px; } .pagination button { @@ -521,4 +755,4 @@ blockquote { .pagination button:disabled { background-color: #ccc; cursor: not-allowed; -} +} \ No newline at end of file