Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions src/components/NetworkStatusBanner.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className={`network-status-banner ${status} ${visible ? "visible" : "hidden"}`}>
<div className="network-status-content">
{status === "offline" ? (
<>
<svg
className="network-icon"
stroke="currentColor"
fill="none"
strokeWidth="2"
viewBox="0 0 24 24"
strokeLinecap="round"
strokeLinejoin="round"
height="1.2em"
width="1.2em"
xmlns="http://www.w3.org/2000/svg"
>
<line x1="1" y1="1" x2="23" y2="23"></line>
<path d="M16.72 11.06A10.94 10.94 0 0 1 19 12.55"></path>
<path d="M5 12.55a10.94 10.94 0 0 1 5.17-2.39"></path>
<path d="M10.71 5.05A16 16 0 0 1 22.58 9"></path>
<path d="M1.42 9a15.91 15.91 0 0 1 4.7-2.88"></path>
<path d="M8.53 16.11a6 6 0 0 1 6.95 0"></path>
<line x1="12" y1="20" x2="12.01" y2="20"></line>
</svg>
<span>You are offline. Working in offline mode.</span>
</>
) : (
<>
<svg
className="network-icon"
stroke="currentColor"
fill="none"
strokeWidth="2"
viewBox="0 0 24 24"
strokeLinecap="round"
strokeLinejoin="round"
height="1.2em"
width="1.2em"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M5 12.55a11 11 0 0 1 14.08 0"></path>
<path d="M1.42 9a16 16 0 0 1 23.16 0"></path>
<path d="M8.53 16.11a6 6 0 0 1 6.95 0"></path>
<line x1="12" y1="20" x2="12.01" y2="20"></line>
</svg>
<span>Back online! Connection restored.</span>
</>
)}
</div>
</div>
);
};

export default NetworkStatusBanner;
20 changes: 20 additions & 0 deletions src/hooks/useOnlineStatus.js
Original file line number Diff line number Diff line change
@@ -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;
}
16 changes: 10 additions & 6 deletions src/pages/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/generator" element={<Generator />} />
<Route path="/contributors" element={<Contributors />} />
<Route path="*" element={<NotFound />} />
</Routes>
<>
<NetworkStatusBanner />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/generator" element={<Generator />} />
<Route path="/contributors" element={<Contributors />} />
<Route path="*" element={<NotFound />} />
</Routes>
</>
);
}

Expand Down
70 changes: 69 additions & 1 deletion src/pages/Contributors.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
{
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -106,7 +126,7 @@ const Contributors = () => {
};

fetchContributors();
}, []);
}, [isOnline]);

return (
<div className="contributors-page">
Expand All @@ -119,6 +139,54 @@ const Contributors = () => {
</p>
</header>

{offlineNotice && (
<div className="offline-notice" role="alert">
<div className="offline-notice-content">
<svg
className="offline-notice-icon"
stroke="currentColor"
fill="none"
strokeWidth="2"
viewBox="0 0 24 24"
strokeLinecap="round"
strokeLinejoin="round"
height="1.2em"
width="1.2em"
xmlns="http://www.w3.org/2000/svg"
>
<line x1="1" y1="1" x2="23" y2="23"></line>
<path d="M16.72 11.06A10.94 10.94 0 0 1 19 12.55"></path>
<path d="M5 12.55a10.94 10.94 0 0 1 5.17-2.39"></path>
<path d="M10.71 5.05A16 16 0 0 1 22.58 9"></path>
<path d="M1.42 9a15.91 15.91 0 0 1 4.7-2.88"></path>
<path d="M8.53 16.11a6 6 0 0 1 6.95 0"></path>
<line x1="12" y1="20" x2="12.01" y2="20"></line>
</svg>
<span>You are viewing cached contributor data because your browser is currently offline.</span>
</div>
<button
onClick={() => setOfflineNotice(false)}
className="offline-notice-close"
aria-label="Dismiss notice"
>
<svg
stroke="currentColor"
fill="none"
strokeWidth="2"
viewBox="0 0 24 24"
strokeLinecap="round"
strokeLinejoin="round"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
)}

<section className="grid">
{contributors.map((contributor) => (
<div key={contributor.id} className="card">
Expand Down
49 changes: 49 additions & 0 deletions src/styles/Contributors.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
77 changes: 77 additions & 0 deletions src/styles/NetworkStatusBanner.css
Original file line number Diff line number Diff line change
@@ -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;
}
}