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
7 changes: 7 additions & 0 deletions backend/models/User.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ const UserSchema = new mongoose.Schema({
type: String,
required: true,
},
bookmarks: [
{
githubUsername: { type: String, required: true },
avatarUrl: { type: String },
savedAt: { type: Date, default: Date.now },
},
],
});

// ✅ FIXED: no next()
Expand Down
75 changes: 75 additions & 0 deletions backend/routes/bookmarks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
const express = require('express');
const router = express.Router();
const User = require('../models/User');

// Simple auth check middleware (session-based via passport)
function ensureAuth(req, res, next) {
if (req.isAuthenticated && req.isAuthenticated()) return next();
return res.status(401).json({ message: 'Authentication required' });
}

// GET /api/bookmarks - returns current user's bookmarks
router.get('/', ensureAuth, async (req, res) => {
try {
console.log('GET /api/bookmarks req.isAuthenticated=', req.isAuthenticated && req.isAuthenticated(), 'user=', req.user && req.user.id);
const user = await User.findById(req.user._id).select('bookmarks');
return res.json({ bookmarks: user?.bookmarks || [] });
} catch (err) {
console.error('Error fetching bookmarks', err);
return res.status(500).json({ message: 'Failed to fetch bookmarks', error: err.message });
}
});

// POST /api/bookmarks - add a bookmark
router.post('/', ensureAuth, async (req, res) => {
try {
console.log('POST /api/bookmarks req.isAuthenticated=', req.isAuthenticated && req.isAuthenticated(), 'user=', req.user && req.user.id, 'body=', req.body);
const { githubUsername, avatarUrl } = req.body;
if (!githubUsername || !githubUsername.trim()) {
return res.status(400).json({ message: 'githubUsername is required' });
}

const user = await User.findById(req.user._id);
if (!user) return res.status(404).json({ message: 'User not found' });

const exists = user.bookmarks?.some(
(b) => b.githubUsername.toLowerCase() === githubUsername.toLowerCase()
);
if (exists) return res.status(409).json({ message: 'Bookmark already exists' });

user.bookmarks = user.bookmarks || [];
user.bookmarks.unshift({ githubUsername, avatarUrl });
await user.save();

return res.status(201).json({ message: 'Bookmark saved', bookmark: user.bookmarks[0] });
} catch (err) {
console.error('Error saving bookmark', err);
return res.status(500).json({ message: 'Failed to save bookmark', error: err.message });
}
});

// DELETE /api/bookmarks/:username - remove bookmark
router.delete('/:username', ensureAuth, async (req, res) => {
try {
console.log('DELETE /api/bookmarks/:username req.isAuthenticated=', req.isAuthenticated && req.isAuthenticated(), 'user=', req.user && req.user.id, 'params=', req.params);
const username = req.params.username;
const user = await User.findById(req.user._id);
if (!user) return res.status(404).json({ message: 'User not found' });

const before = (user.bookmarks || []).length;
user.bookmarks = (user.bookmarks || []).filter(
(b) => b.githubUsername.toLowerCase() !== username.toLowerCase()
);

if (user.bookmarks.length === before) {
return res.status(404).json({ message: 'Bookmark not found' });
}

await user.save();
return res.json({ message: 'Bookmark removed' });
} catch (err) {
return res.status(500).json({ message: 'Failed to remove bookmark', error: err.message });
}
});

module.exports = router;
22 changes: 17 additions & 5 deletions backend/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,20 @@ const logger = require('./logger');

const app = express();

// CORS configuration
const allowedOrigins = ['http://localhost:5173', 'https://github-spy.etlify.app'];
// CORS configuration - allow common local dev origins
const allowedOrigins = [
'http://localhost:5173',
'http://127.0.0.1:5173',
'http://localhost:5174',
'https://github-spy.etlify.app'
];
app.use(cors({
origin: function (origin, callback) {
// Allow requests with no origin like curl/postman
if (!origin || allowedOrigins.indexOf(origin) !== -1) {
callback(null, true);
} else{
callback(new Error('Blocked by CORS policy'));
} else {
callback(new Error('Blocked by CORS policy: ' + origin));
}
},
credentials: true
Expand All @@ -29,16 +35,22 @@ app.use(cors({
// Middleware
app.use(bodyParser.json());
app.use(session({
secret: process.env.SESSION_SECRET,
secret: process.env.SESSION_SECRET || 'dev-secret',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Prevent hardcoded session secret in production.

The fallback 'dev-secret' is a security risk if the SESSION_SECRET environment variable is not set in production. A weak or known secret enables session forgery and hijacking attacks.

🔒 Enforce SESSION_SECRET in production

Option 1 (recommended): Fail fast in production if secret is missing:

 app.use(session({
-    secret: process.env.SESSION_SECRET || 'dev-secret',
+    secret: process.env.SESSION_SECRET || (() => {
+        if (process.env.NODE_ENV === 'production') {
+            throw new Error('SESSION_SECRET must be set in production');
+        }
+        logger.warn('Using dev-secret for session - DO NOT use in production');
+        return 'dev-secret';
+    })(),
     resave: false,
     saveUninitialized: false,

Option 2 (simpler): Require SESSION_SECRET always:

-    secret: process.env.SESSION_SECRET || 'dev-secret',
+    secret: process.env.SESSION_SECRET,

And document in README/env.example that developers must set SESSION_SECRET=dev-secret locally.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
secret: process.env.SESSION_SECRET || 'dev-secret',
secret: process.env.SESSION_SECRET,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/server.js` at line 38, The session secret currently falls back to a
hardcoded 'dev-secret' (secret: process.env.SESSION_SECRET || 'dev-secret')
which is unsafe; change the logic to require SESSION_SECRET in production by
throwing an error if process.env.NODE_ENV === 'production' and
process.env.SESSION_SECRET is falsy, or remove the default entirely and always
use process.env.SESSION_SECRET so the app fails fast when missing; update the
code paths that reference secret to use only process.env.SESSION_SECRET (or the
validated value) and add a startup check that validates SESSION_SECRET when the
server initializes.

resave: false,
saveUninitialized: false,
cookie: {
sameSite: 'lax', // help ensure cross-port cookies work in dev
},
}));
app.use(passport.initialize());
app.use(passport.session());

// Routes
const authRoutes = require('./routes/auth');
app.use('/api/auth', authRoutes);
// Bookmarks
const bookmarkRoutes = require('./routes/bookmarks');
app.use('/api/bookmarks', bookmarkRoutes);

// Connect to MongoDB
mongoose.connect(process.env.MONGO_URI, {}).then(() => {
Expand Down
4 changes: 3 additions & 1 deletion src/Routes/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import Signup from "../pages/Signup/Signup.tsx";
import Login from "../pages/Login/Login.tsx";
import ContributorProfile from "../pages/ContributorProfile/ContributorProfile.tsx";
import Home from "../pages/Home/Home.tsx";
import Activity from "../pages/Activity.tsx";
import Activity from "../pages/Activity.tsx";
import Bookmarks from "../pages/Bookmarks";
import PrivacyPolicy from "../pages/Privacy/PrivacyPolicy.tsx"; // ✅ Updated import path to match your new folder structure

const Router = () => {
Expand All @@ -22,6 +23,7 @@ const Router = () => {
<Route path="/contributors" element={<Contributors />} />
<Route path="/contributor/:username" element={<ContributorProfile />} />
<Route path="/activity" element={<Activity />} />
<Route path="/bookmarks" element={<Bookmarks />} />
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether an existing route guard/auth wrapper already exists to reuse.
rg -n --type=tsx -C3 '\b(ProtectedRoute|PrivateRoute|RequireAuth|AuthGuard)\b|isAuthenticated|currentUser|/login' src

Repository: GitMetricsLab/github_tracker

Length of output: 100


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect Router.tsx around the referenced line
echo "---- Router.tsx (context around /bookmarks) ----"
if [ -f "src/Routes/Router.tsx" ]; then
  nl -ba src/Routes/Router.tsx | sed -n '1,120p' | sed -n '15,45p'
else
  echo "Missing: src/Routes/Router.tsx" >&2
  exit 1
fi

echo "---- Search for existing auth/route guards in TSX files ----"
rg -n -g'*.{ts,tsx}' -C3 '\b(ProtectedRoute|PrivateRoute|RequireAuth|AuthGuard)\b|isAuthenticated|currentUser|/login' src || true

echo "---- Search specifically for route protection patterns (auth + redirect) ----"
rg -n -g'*.{ts,tsx}' -C3 '(Navigate\s+to=|redirect|/login|useAuth|useUser|auth|session)' src/Routes src || true

Repository: GitMetricsLab/github_tracker

Length of output: 182


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- Check file exists ----"
ls -la src/Routes/Router.tsx || true

echo "---- Router.tsx (context around /bookmarks) ----"
if [ -f "src/Routes/Router.tsx" ]; then
  cat -n src/Routes/Router.tsx | sed -n '15,45p'
fi

echo "---- Search for existing auth/route guards in TS/TSX files ----"
rg -n -g'*.ts' -g'*.tsx' -C3 '\b(ProtectedRoute|PrivateRoute|RequireAuth|AuthGuard)\b|isAuthenticated|currentUser|/login' src || true

echo "---- Search specifically for route protection patterns (auth + redirect) ----"
rg -n -g'*.ts' -g'*.tsx' -C3 '(Navigate\s+to=|redirect|/login|useAuth|useUser|auth|session)' src/Routes src || true

Repository: GitMetricsLab/github_tracker

Length of output: 14988


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- Inspect src/App.tsx (routing/auth gating) ----"
cat -n src/App.tsx | sed -n '1,120p'

echo "---- Inspect src/pages/Bookmarks/* (if exists) ----"
ls -la src/pages/Bookmarks || true
fd -t f "Bookmarks.*" src/pages || true
fd -t f "bookmarks" src/pages -i || true

# Show Bookmarks route component if present
if [ -d "src/pages/Bookmarks" ]; then
  fd -t f . src/pages/Bookmarks --max-depth 2 -i || true
fi
# fallback: search for "function Bookmarks" or "const Bookmarks"
rg -n -g'*.{ts,tsx}' 'function\s+Bookmarks|const\s+Bookmarks|export\s+default\s+Bookmarks' src/pages src/components src -S || true

Repository: GitMetricsLab/github_tracker

Length of output: 2104


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- Router.tsx (full) ----"
cat -n src/Routes/Router.tsx

echo "---- Bookmarks.tsx (key sections) ----"
cat -n src/pages/Bookmarks.tsx | sed -n '1,200p'

echo "---- Search for routing-level auth wrappers/components ----"
rg -n -g'*.{ts,tsx}' '\b(ProtectedRoute|PrivateRoute|RequireAuth|AuthGuard)\b' src || true

echo "---- Search for auth checks / redirects used in pages/components ----"
rg -n -g'*.{ts,tsx}' 'Navigate\s+to=|/login|redirect|withCredentials|session|token|useGitHubAuth|useAuth|isAuthenticated|currentUser' src || true

echo "---- Auth usage within Bookmarks.tsx ----"
rg -n -g'*.tsx' 'useGitHubAuth|token|authError|Navigate\s+to=|/login|withCredentials|axios|fetch' src/pages/Bookmarks.tsx || true

Repository: GitMetricsLab/github_tracker

Length of output: 9650


Protect the /bookmarks route with an auth guard (redirect to /login).

src/Routes/Router.tsx renders /bookmarks directly to <Bookmarks /> with no ProtectedRoute/PrivateRoute/AuthGuard-style wrapper in src. src/pages/Bookmarks.tsx calls /api/bookmarks and only shows “Failed to load bookmarks” on failure, so unauthenticated users get an API-error experience instead of access control.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Routes/Router.tsx` at line 26, The /bookmarks route in Router.tsx is
unprotected; wrap the Bookmarks element with your auth guard so unauthenticated
users are redirected to /login (e.g., replace <Route path="/bookmarks"
element={<Bookmarks />} /> with something like
element={<ProtectedRoute><Bookmarks/></ProtectedRoute>} or use a component that
returns <Navigate to="/login" /> when auth fails). Ensure the guard you use
(ProtectedRoute/PrivateRoute/AuthGuard) checks the same auth state your app uses
and performs the redirect to "/login" so Bookmarks.tsx no longer has to handle
access control via API errors.


{/* Privacy Policy page route */}
<Route path="/privacy" element={<PrivacyPolicy />} />
Expand Down
88 changes: 84 additions & 4 deletions src/components/ActivityFeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ interface EventType {
export default function ActivityFeed({ username }: { username: string }) {
const [events, setEvents] = useState<EventType[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [isBookmarked, setIsBookmarked] = useState(false);
const [bookmarkLoading, setBookmarkLoading] = useState(false);

const backendUrl = import.meta.env.VITE_BACKEND_URL || 'http://localhost:5000';

// 🕒 time ago function
const getTimeAgo = (dateString: string) => {
Expand All @@ -26,6 +31,24 @@ export default function ActivityFeed({ username }: { username: string }) {
};

useEffect(() => {
// check bookmark state for this username
const checkBookmark = async () => {
if (!username.trim()) return setIsBookmarked(false);
try {
const res = await fetch(`${backendUrl}/api/bookmarks`, { credentials: 'include' });
if (!res.ok) return setIsBookmarked(false);
const data = await res.json();
const found = (data.bookmarks || []).some(
(b: any) => b.githubUsername.toLowerCase() === username.toLowerCase()
);
setIsBookmarked(!!found);
} catch (err) {
// ignore
Comment on lines +40 to +46
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and show the relevant code in ActivityFeed.tsx
FILE="src/components/ActivityFeed.tsx"
if [ ! -f "$FILE" ]; then
  echo "Missing file: $FILE" >&2
  exit 1
fi

echo "---- $FILE (lines 25-80) ----"
nl -ba "$FILE" | sed -n '25,80p'

echo "---- grep for catch bindings in ActivityFeed.tsx ----"
rg -n "catch\s*\([^)]*\)" "$FILE" || true

echo "---- grep for 'any' in ActivityFeed.tsx ----"
rg -n "\bany\b" "$FILE" || true

Repository: GitMetricsLab/github_tracker

Length of output: 190


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/components/ActivityFeed.tsx"
if [ ! -f "$FILE" ]; then
  echo "Missing file: $FILE" >&2
  exit 1
fi

echo "---- $FILE (lines 25-80) ----"
cat -n "$FILE" | sed -n '25,80p'

echo "---- grep for catch bindings in ActivityFeed.tsx ----"
rg -n "catch\s*\([^)]*\)" "$FILE" || true

echo "---- grep for 'any' in ActivityFeed.tsx ----"
rg -n "\bany\b" "$FILE" || true

Repository: GitMetricsLab/github_tracker

Length of output: 2479


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/components/ActivityFeed.tsx"
echo "---- $FILE (lines 80-160) ----"
cat -n "$FILE" | sed -n '80,160p'

echo "---- grep for 'any' in $FILE ----"
rg -n "\bany\b" "$FILE" || true

Repository: GitMetricsLab/github_tracker

Length of output: 3425


Type bookmark payload and remove unused catch parameter in the bookmark check

  • Replace (b: any) with a typed bookmark payload so githubUsername is properly typed.
  • In the checkBookmark path, catch (err) declares err but it’s unused (the block just ignores), so switch to catch {}.
Suggested fix
+type BookmarkPayload = { bookmarks?: Array<{ githubUsername: string }> };
+
 ...
-        const data = await res.json();
+        const data: BookmarkPayload = await res.json();
         const found = (data.bookmarks || []).some(
-          (b: any) => b.githubUsername.toLowerCase() === username.toLowerCase()
+          (b) => b.githubUsername.toLowerCase() === username.toLowerCase()
         );
         setIsBookmarked(!!found);
-      } catch (err) {
+      } catch {
         // ignore
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const data = await res.json();
const found = (data.bookmarks || []).some(
(b: any) => b.githubUsername.toLowerCase() === username.toLowerCase()
);
setIsBookmarked(!!found);
} catch (err) {
// ignore
type BookmarkPayload = { bookmarks?: Array<{ githubUsername: string }> };
const data: BookmarkPayload = await res.json();
const found = (data.bookmarks || []).some(
(b) => b.githubUsername.toLowerCase() === username.toLowerCase()
);
setIsBookmarked(!!found);
} catch {
// ignore
}
🧰 Tools
🪛 ESLint

[error] 41-41: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 44-44: 'err' is defined but never used.

(@typescript-eslint/no-unused-vars)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ActivityFeed.tsx` around lines 39 - 45, In ActivityFeed.tsx,
update the bookmark check to use a proper typed payload instead of (b: any) and
remove the unused catch parameter: define or import an interface like Bookmark {
githubUsername: string } and change the some callback to (b: Bookmark) =>
b.githubUsername.toLowerCase() === username.toLowerCase(); also replace the
unused catch (err) { /* ignore */ } with catch { } to avoid an unused variable;
keep the existing setIsBookmarked(!!found) logic intact.

}
};

checkBookmark();

const fetchEvents = async () => {
try {
setLoading(true);
Expand All @@ -49,11 +72,68 @@ export default function ActivityFeed({ username }: { username: string }) {
return () => clearInterval(interval);
}, [username]);

const handleAddBookmark = async () => {
if (!username.trim()) return;
setBookmarkLoading(true);
try {
const res = await fetch(`${backendUrl}/api/bookmarks`, {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ githubUsername: username }),
});
if (!res.ok) throw new Error('Failed to save');
setIsBookmarked(true);
} catch (err) {
console.error(err);
} finally {
setBookmarkLoading(false);
}
};

const handleRemoveBookmark = async () => {
if (!username.trim()) return;
setBookmarkLoading(true);
try {
const res = await fetch(`${backendUrl}/api/bookmarks/${encodeURIComponent(username)}`, {
method: 'DELETE',
credentials: 'include',
});
if (!res.ok) throw new Error('Failed to remove');
setIsBookmarked(false);
} catch (err) {
console.error(err);
} finally {
setBookmarkLoading(false);
}
};

const currentEvents = events.slice(0, 10);

return (
<div className="p-4">
<h2 className="text-xl font-bold mb-4 text-center">
Activity Feed
</h2>
<div className="rounded-[2rem] border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-900 p-6 shadow-lg">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between mb-5">
<div className="flex items-center gap-3">
<h2 className="text-2xl font-bold">Activity Feed</h2>
<p className="text-sm text-gray-500 dark:text-gray-400">
Tracking <span className="font-semibold text-gray-900 dark:text-white">{username}</span>
</p>
</div>

<div className="flex items-center gap-3">
<p className="text-xs uppercase tracking-[0.2em] text-gray-400 mr-3">Refreshes every 30s</p>

<button
onClick={isBookmarked ? handleRemoveBookmark : handleAddBookmark}
disabled={bookmarkLoading}
title={isBookmarked ? 'Remove bookmark' : 'Save bookmark'}
className={`flex items-center gap-2 px-3 py-1.5 rounded-full transition ${isBookmarked ? 'bg-yellow-500 text-white' : 'bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-white'}`}
>
<span className="text-lg">{isBookmarked ? '★' : '☆'}</span>
<span className="text-sm font-medium">{isBookmarked ? 'Saved' : 'Save'}</span>
</button>
</div>
</div>

{loading ? (
<p className="text-center">Loading...</p>
Expand Down
35 changes: 21 additions & 14 deletions src/components/Hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,17 @@ const MATRIX_CELLS = Array.from({ length: 21 }, (_, i) => i);
const Hero = () => {
return (
<section className="relative overflow-hidden bg-slate-50 dark:bg-[#030712] text-slate-900 dark:text-white px-6 py-20 lg:py-32 min-h-[90vh] flex items-center transition-colors duration-500">

{/* 1. Cyber Grid Overlay */}
<div className="absolute inset-0 bg-[linear-gradient(to_right,#e2e8f0_1px,transparent_1px),linear-gradient(to_bottom,#e2e8f0_1px,transparent_1px)] dark:bg-[linear-gradient(to_right,#1f293710_1px,transparent_1px),linear-gradient(to_bottom,#1f293710_1px,transparent_1px)] bg-[size:4rem_4rem] [mask-image:radial-gradient(ellipse_60%_50%_at_50%_0%,#000_70%,transparent_100%)] pointer-events-none" />

{/* Ambient Radial Glow Elements */}
<div className="absolute top-1/4 left-1/4 -translate-x-1/2 -translate-y-1/2 w-[500px] h-[500px] bg-blue-400/10 dark:bg-blue-500/10 rounded-full blur-[120px] pointer-events-none" />
<div className="absolute bottom-1/4 right-1/4 translate-x-1/2 translate-y-1/2 w-[600px] h-[600px] bg-cyan-400/10 dark:bg-cyan-500/10 rounded-full blur-[160px] pointer-events-none" />

<div className="relative max-w-7xl mx-auto w-full z-10">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12 lg:gap-8 items-center">

{/* LEFT COLUMN: Typography & CTA */}
<div className="lg:col-span-6 text-left space-y-6 max-w-2xl mx-auto lg:mx-0">
<h1 className="text-4xl md:text-5xl xl:text-6xl font-extrabold tracking-tight leading-none text-slate-900 dark:text-slate-100">
Expand All @@ -26,9 +26,9 @@ const Hero = () => {
Like Never Before
</span>
</h1>

<p className="text-base md:text-lg text-slate-600 dark:text-slate-400 leading-relaxed max-w-xl">
Monitor and analyze GitHub user activity with powerful insights. Perfect for developers,
Monitor and analyze GitHub user activity with powerful insights. Perfect for developers,
project managers, and teams who want to understand contribution patterns and repository engagement.
</p>

Expand All @@ -40,12 +40,19 @@ const Hero = () => {
<span className="text-slate-900 dark:text-white group-hover:text-white">Start Tracking</span>
</span>
</Link>

<Link
to="/#features"
className="inline-flex items-center justify-center rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 px-6 py-3 text-sm font-semibold text-slate-700 dark:text-gray-200 hover:border-blue-300 hover:text-blue-600 dark:hover:text-blue-300 transition-colors duration-200"
>
Explore Features
</Link>
</div>
</div>

{/* RIGHT COLUMN: Dashboard & Floating Git Elements */}
<div className="lg:col-span-6 relative flex items-center justify-center h-[400px] md:h-[500px]">

{/* The Main Dashboard Mockup Card */}
<div className="relative w-full max-w-md bg-white/70 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200 dark:border-slate-800 rounded-2xl p-6 shadow-xl dark:shadow-2xl dark:shadow-cyan-950/20 transform hover:scale-[1.02] transition-transform duration-500 z-10">
<div className="flex items-center justify-between pb-4 border-b border-slate-200 dark:border-slate-800/60 mb-6">
Expand Down Expand Up @@ -78,14 +85,14 @@ const Hero = () => {
// Fully qualified class names to ensure they aren't removed by Tailwind's compilation process
const intensitiesLight = ['bg-slate-200', 'bg-emerald-200', 'bg-emerald-300', 'bg-emerald-500', 'bg-emerald-600'];
const intensitiesDark = ['dark:bg-slate-800', 'dark:bg-emerald-900/60', 'dark:bg-emerald-700', 'dark:bg-emerald-500', 'dark:bg-emerald-400'];

const lightClass = intensitiesLight[cellIndex % intensitiesLight.length];
const darkClass = intensitiesDark[cellIndex % intensitiesDark.length];

return (
<div
key={cellIndex}
className={`w-3 h-3 rounded-[2px] transition-all duration-300 hover:scale-125 ${lightClass} ${darkClass}`}
<div
key={cellIndex}
className={`w-3 h-3 rounded-[2px] transition-all duration-300 hover:scale-125 ${lightClass} ${darkClass}`}
/>
);
})}
Expand All @@ -95,21 +102,21 @@ const Hero = () => {
</div>

{/* Floating Git Icon Nodes using native inline styles for correct staggered delays */}
<div
<div
style={{ animationDelay: '0s' }}
className="absolute -top-4 left-6 md:left-12 bg-white dark:bg-slate-900/80 backdrop-blur-md p-3 rounded-xl border border-slate-200 dark:border-slate-800 shadow-md dark:shadow-lg z-20 animate-bounce"
>
<GitBranch className="w-6 h-6 text-blue-600 dark:text-blue-400" />
</div>

<div
<div
style={{ animationDelay: '1s' }}
className="absolute bottom-12 left-2 bg-white dark:bg-slate-900/80 backdrop-blur-md p-3 rounded-xl border border-slate-200 dark:border-slate-800 shadow-md dark:shadow-lg z-20 animate-bounce"
>
<GitCommit className="w-6 h-6 text-emerald-600 dark:text-emerald-400" />
</div>

<div
<div
style={{ animationDelay: '0.5s' }}
className="absolute top-12 right-6 bg-white dark:bg-slate-900/80 backdrop-blur-md p-3 rounded-xl border border-slate-200 dark:border-slate-800 shadow-md dark:shadow-lg z-20 animate-bounce"
>
Expand Down
Loading