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
184 changes: 152 additions & 32 deletions src/components/ActivityFeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,12 @@ 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) => {
const diff = Math.floor(
(Date.now() - new Date(dateString).getTime()) / 1000
Expand All @@ -26,19 +30,69 @@ 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 +39 to +45
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 () => {
if (!username.trim()) {
setEvents([]);
setError("Please enter a GitHub username to get started.");
setLoading(false);
return;
}

try {
setLoading(true);
setError("");

const res = await fetch(
`https://api.github.com/users/${username}/events`
);
Comment on lines 63 to 65
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

FILE="src/components/ActivityFeed.tsx"

echo "== Lines 50-90 =="
nl -ba "$FILE" | sed -n '50,90p'

echo
echo "== Search for encodeURIComponent(username) in repo =="
rg -n "encodeURIComponent\s*\(\s*username\s*\)" .

echo
echo "== Search for username usage/sanitization in this file =="
rg -n "\busername\b" "$FILE"

Repository: GitMetricsLab/github_tracker

Length of output: 151


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/components/ActivityFeed.tsx"

echo "== Lines 50-90 =="
cat -n "$FILE" | sed -n '50,90p'

echo
echo "== Search for encodeURIComponent(username) in repo =="
rg -n "encodeURIComponent\s*\(\s*username\s*\)" .

echo
echo "== Search for username usage/sanitization in this file =="
rg -n "\busername\b" "$FILE"

Repository: GitMetricsLab/github_tracker

Length of output: 2740


Encode username when building the GitHub Events URL

username is interpolated raw into https://api.github.com/users/${username}/events; encode it to handle special characters and avoid malformed requests.

Suggested fix
-        const res = await fetch(
-          `https://api.github.com/users/${username}/events`
-        );
+        const res = await fetch(
+          `https://api.github.com/users/${encodeURIComponent(username)}/events`
+        );
📝 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 res = await fetch(
`https://api.github.com/users/${username}/events`
);
const res = await fetch(
`https://api.github.com/users/${encodeURIComponent(username)}/events`
);
🤖 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 63 - 65, The GitHub events URL
is built by interpolating the raw username into the fetch call, which can
produce malformed requests for special characters; update the fetch invocation
in the ActivityFeed component to encode the username (use
encodeURIComponent(username)) when constructing
`https://api.github.com/users/.../events` so the request is safe for all
inputs—locate the fetch call that references the `username` variable in
ActivityFeed (or any helper like fetchEvents) and replace the direct
interpolation with an encoded username.


if (!res.ok) {
let message = "Unable to load activity. Please try again.";
if (res.status === 404) {
message = "GitHub user not found. Please check the username.";
} else if (res.status === 403) {
message =
"GitHub rate limit exceeded. Wait a moment and try again.";
}
setEvents([]);
setError(message);
setLoading(false);
return;
}

const data = await res.json();

if (!Array.isArray(data)) {
setError("Unexpected response from GitHub. Please try again.");
setEvents([]);
setLoading(false);
return;
}

setEvents(data);
setLoading(false);
} catch (err) {
console.error(err);
setError("Unable to fetch activity. Check your connection and try again.");
setEvents([]);
} finally {
setLoading(false);
}
};
Expand All @@ -49,40 +103,106 @@ 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>
) : events.length === 0 ? (
<p className="text-center">No activity found</p>
<div className="rounded-3xl border border-dashed border-indigo-300 bg-indigo-50/70 p-6 text-center text-indigo-700 dark:border-indigo-500/50 dark:bg-indigo-950/40 dark:text-indigo-200">
Loading GitHub activity...
</div>
) : error ? (
<div className="rounded-3xl border border-red-200 bg-red-50 p-6 text-center text-red-700 dark:border-red-600/40 dark:bg-red-950/20 dark:text-red-200">
{error}
</div>
) : currentEvents.length === 0 ? (
<div className="rounded-3xl border border-gray-200 bg-gray-50 p-6 text-center text-gray-600 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300">
No recent public activity found for this user.
</div>
) : (
events.slice(0, 10).map((event) => (
<div
key={event.id}
className="border rounded-lg p-3 mb-3 shadow-sm bg-white dark:bg-gray-700"
>
<p className="text-sm font-semibold">
{event.type === "PushEvent" && "🚀 Commit pushed"}
{event.type === "PullRequestEvent" && "🔀 Pull Request"}
{event.type === "IssuesEvent" && "🐛 Issue"}
{event.type === "WatchEvent" && "⭐ Starred repo"}
{![
"PushEvent",
"PullRequestEvent",
"IssuesEvent",
"WatchEvent",
].includes(event.type) && event.type}
</p>

<p className="text-xs text-gray-500 mt-1">
{event.repo?.name} • {getTimeAgo(event.created_at)}
</p>
</div>
))
<div className="space-y-3">
{currentEvents.map((event) => (
<div
key={event.id}
className="rounded-3xl border border-gray-200 bg-gray-50 p-4 shadow-sm transition hover:border-indigo-300 dark:border-gray-700 dark:bg-gray-800"
>
<p className="text-sm font-semibold text-gray-900 dark:text-white">
{event.type === "PushEvent" && "🚀 Commit pushed"}
{event.type === "PullRequestEvent" && "🔀 Pull request event"}
{event.type === "IssuesEvent" && "🐛 Issue event"}
{event.type === "WatchEvent" && "⭐ Starred repository"}
{![
"PushEvent",
"PullRequestEvent",
"IssuesEvent",
"WatchEvent",
].includes(event.type) && event.type}
</p>
<p className="mt-2 text-sm text-gray-500 dark:text-gray-400">
{event.repo?.name || "Unknown repository"} • {getTimeAgo(event.created_at)}
</p>
</div>
))}
</div>
)}
</div>
);
Expand Down
Loading
Loading