Skip to content
Closed
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',
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 />} />

{/* 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
}
};

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`
);

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