diff --git a/src/components/NetworkStatusBanner.jsx b/src/components/NetworkStatusBanner.jsx new file mode 100644 index 0000000..65cb46a --- /dev/null +++ b/src/components/NetworkStatusBanner.jsx @@ -0,0 +1,84 @@ +import React, { useState, useEffect, useRef } from "react"; +import { useOnlineStatus } from "../hooks/useOnlineStatus"; +import "../styles/NetworkStatusBanner.css"; + +const NetworkStatusBanner = () => { + const isOnline = useOnlineStatus(); + const [visible, setVisible] = useState(false); + const [status, setStatus] = useState("online"); // "online" or "offline" + const hasBeenOffline = useRef(false); + + useEffect(() => { + if (!isOnline) { + hasBeenOffline.current = true; + setStatus("offline"); + setVisible(true); + } else { + if (hasBeenOffline.current) { + setStatus("online"); + setVisible(true); + const timer = setTimeout(() => { + setVisible(false); + }, 3000); + return () => clearTimeout(timer); + } + } + }, [isOnline]); + + if (!visible && isOnline) return null; + + return ( +
+
+ {status === "offline" ? ( + <> + + + + + + + + + + You are offline. Working in offline mode. + + ) : ( + <> + + + + + + + Back online! Connection restored. + + )} +
+
+ ); +}; + +export default NetworkStatusBanner; diff --git a/src/hooks/useOnlineStatus.js b/src/hooks/useOnlineStatus.js new file mode 100644 index 0000000..75c9bc7 --- /dev/null +++ b/src/hooks/useOnlineStatus.js @@ -0,0 +1,20 @@ +import { useState, useEffect } from "react"; + +export function useOnlineStatus() { + const [isOnline, setIsOnline] = useState(navigator.onLine); + + useEffect(() => { + const handleOnline = () => setIsOnline(true); + const handleOffline = () => setIsOnline(false); + + window.addEventListener("online", handleOnline); + window.addEventListener("offline", handleOffline); + + return () => { + window.removeEventListener("online", handleOnline); + window.removeEventListener("offline", handleOffline); + }; + }, []); + + return isOnline; +} diff --git a/src/pages/App.jsx b/src/pages/App.jsx index 6a6d986..a9a6f96 100644 --- a/src/pages/App.jsx +++ b/src/pages/App.jsx @@ -4,15 +4,19 @@ import Home from "./Home"; import Generator from "./Generator"; import Contributors from "./Contributors"; import NotFound from "./NotFound"; +import NetworkStatusBanner from "../components/NetworkStatusBanner"; function App() { return ( - - } /> - } /> - } /> - } /> - + <> + + + } /> + } /> + } /> + } /> + + ); } diff --git a/src/pages/Contributors.jsx b/src/pages/Contributors.jsx index 46d98f3..69652f0 100644 --- a/src/pages/Contributors.jsx +++ b/src/pages/Contributors.jsx @@ -2,6 +2,7 @@ import React, { useState, useEffect } from "react"; import { Link, NavLink } from "react-router-dom"; import "../styles/Contributors.css"; import Navbar from "../components/Navbar"; +import { useOnlineStatus } from "../hooks/useOnlineStatus"; const fallbackContributors = [ { @@ -43,8 +44,27 @@ const fallbackContributors = [ const Contributors = () => { const [contributors, setContributors] = useState(fallbackContributors); + const isOnline = useOnlineStatus(); + const [offlineNotice, setOfflineNotice] = useState(!navigator.onLine); useEffect(() => { + if (!isOnline) { + setOfflineNotice(true); + const cachedData = localStorage.getItem("github_contributors"); + if (cachedData) { + try { + const parsedData = JSON.parse(cachedData); + if (Array.isArray(parsedData) && parsedData.length > 0) { + setContributors(parsedData); + } + } catch (parseErr) { + console.error("Failed to parse cached contributors:", parseErr); + } + } + return; + } + + setOfflineNotice(false); const fetchContributors = async () => { try { const cachedData = localStorage.getItem("github_contributors"); @@ -106,7 +126,7 @@ const Contributors = () => { }; fetchContributors(); - }, []); + }, [isOnline]); return (
@@ -119,6 +139,54 @@ const Contributors = () => {

+ {offlineNotice && ( +
+
+ + + + + + + + + + You are viewing cached contributor data because your browser is currently offline. +
+ +
+ )} +
{contributors.map((contributor) => (
diff --git a/src/styles/Contributors.css b/src/styles/Contributors.css index 7b148b3..0a50b36 100644 --- a/src/styles/Contributors.css +++ b/src/styles/Contributors.css @@ -238,3 +238,52 @@ padding-top: 6rem; } } + +/* Inline notice for offline/cached status */ +.offline-notice { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + background: rgba(245, 158, 11, 0.08); + border: 1px solid rgba(245, 158, 11, 0.25); + border-radius: 12px; + padding: 1rem 1.5rem; + margin: 0 auto 3rem; + max-width: 800px; + width: 90%; + color: #f59e0b; + font-size: 0.95rem; + font-weight: 500; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); + animation: fadeInUp 0.4s ease-out; +} + +.offline-notice-content { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.offline-notice-icon { + flex-shrink: 0; + color: #fbbf24; +} + +.offline-notice-close { + background: none; + border: none; + color: #a1a1aa; + cursor: pointer; + padding: 4px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + transition: all 0.2s; +} + +.offline-notice-close:hover { + background: rgba(255, 255, 255, 0.08); + color: var(--text-primary); +} diff --git a/src/styles/NetworkStatusBanner.css b/src/styles/NetworkStatusBanner.css new file mode 100644 index 0000000..c4de253 --- /dev/null +++ b/src/styles/NetworkStatusBanner.css @@ -0,0 +1,77 @@ +.network-status-banner { + position: fixed; + bottom: 24px; + left: 50%; + transform: translateX(-50%) translateY(100px); + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; + padding: 0.75rem 1.5rem; + border-radius: 12px; + font-family: inherit; + font-size: 0.95rem; + font-weight: 500; + box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.5), 0 8px 10px -6px rgba(0, 0, 0, 0.5); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.4s ease, border-color 0.3s ease, box-shadow 0.3s ease; + pointer-events: none; + opacity: 0; +} + +.network-status-banner.visible { + transform: translateX(-50%) translateY(0); + opacity: 1; +} + +.network-status-banner.hidden { + transform: translateX(-50%) translateY(100px); + opacity: 0; +} + +/* Offline state (Amber/Red) */ +.network-status-banner.offline { + background: rgba(220, 38, 38, 0.1); + border: 1px solid rgba(220, 38, 38, 0.3); + color: #f87171; + box-shadow: 0 0 20px rgba(220, 38, 38, 0.15), 0 10px 25px -5px rgba(0, 0, 0, 0.5); +} + +/* Online state (Emerald Green) */ +.network-status-banner.online { + background: rgba(16, 185, 129, 0.1); + border: 1px solid rgba(16, 185, 129, 0.3); + color: #34d399; + box-shadow: 0 0 20px rgba(16, 185, 129, 0.15), 0 10px 25px -5px rgba(0, 0, 0, 0.5); +} + +.network-status-content { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.network-icon { + flex-shrink: 0; + animation: pulse-icon 2s infinite ease-in-out; +} + +.network-status-banner.offline .network-icon { + color: #ef4444; +} + +.network-status-banner.online .network-icon { + color: #10b981; +} + +@keyframes pulse-icon { + 0%, 100% { + transform: scale(1); + opacity: 1; + } + 50% { + transform: scale(1.08); + opacity: 0.8; + } +}