-
-
+
+
+
Live GitHub Activity
-
+
+
+ Enter any GitHub username below to track recent public activity in real time.
+
+
+
+
+
+
);
diff --git a/src/pages/Bookmarks.tsx b/src/pages/Bookmarks.tsx
new file mode 100644
index 00000000..d3329572
--- /dev/null
+++ b/src/pages/Bookmarks.tsx
@@ -0,0 +1,90 @@
+import React, { useEffect, useState, useContext } from 'react';
+import { ThemeContext } from '../context/ThemeContext';
+import type { ThemeContextType } from '../context/ThemeContext';
+
+const backendUrl = import.meta.env.VITE_BACKEND_URL || 'http://localhost:5000';
+
+interface Bookmark {
+ githubUsername: string;
+ avatarUrl?: string;
+ savedAt?: string;
+}
+
+export default function Bookmarks() {
+ const [bookmarks, setBookmarks] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState('');
+
+ const themeContext = useContext(ThemeContext) as ThemeContextType;
+ const { mode } = themeContext;
+
+ const fetchBookmarks = async () => {
+ setLoading(true);
+ setError('');
+ try {
+ const res = await fetch(`${backendUrl}/api/bookmarks`, { credentials: 'include' });
+ if (!res.ok) throw new Error(await res.text());
+ const data = await res.json();
+ setBookmarks(data.bookmarks || []);
+ } catch (err: unknown) {
+ setError('Failed to load bookmarks');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchBookmarks();
+ }, []);
+
+ const handleRemove = async (username: string) => {
+ try {
+ const res = await fetch(`${backendUrl}/api/bookmarks/${encodeURIComponent(username)}`, {
+ method: 'DELETE',
+ credentials: 'include',
+ });
+ if (!res.ok) throw new Error('Failed');
+ setBookmarks((prev) => prev.filter((b) => b.githubUsername.toLowerCase() !== username.toLowerCase()));
+ } catch (err) {
+ setError('Unable to remove bookmark');
+ }
+ };
+
+ return (
+
+
+
Saved Bookmarks
+
+
+ {loading ? (
+
Loading...
+ ) : error ? (
+
{error}
+ ) : bookmarks.length === 0 ? (
+
No bookmarks yet.
+ ) : (
+
+ )}
+
+
+
+ );
+}
diff --git a/src/pages/Home/Home.tsx b/src/pages/Home/Home.tsx
index 03759ab4..52ead281 100644
--- a/src/pages/Home/Home.tsx
+++ b/src/pages/Home/Home.tsx
@@ -1,16 +1,29 @@
+import { useEffect } from "react";
+import { useLocation } from "react-router-dom";
import Hero from "../../components/Hero";
import HowItWorks from "../../components/HowItWorks";
import Features from "../../components/Features";
function Home() {
+ const location = useLocation();
+
+ useEffect(() => {
+ if (!location.hash) return;
+
+ const id = location.hash.replace("#", "");
+ const element = document.getElementById(id);
+ if (element) {
+ element.scrollIntoView({ behavior: "smooth", block: "start" });
+ }
+ }, [location.hash]);
+
return (
-
-
-
-
+
+
+
- )
+ );
}
-export default Home
+export default Home;
diff --git a/src/pages/Login/Login.tsx b/src/pages/Login/Login.tsx
index 92b7073e..643b1287 100644
--- a/src/pages/Login/Login.tsx
+++ b/src/pages/Login/Login.tsx
@@ -4,7 +4,7 @@ import { useNavigate, Link } from "react-router-dom";
import { ThemeContext } from "../../context/ThemeContext";
import type { ThemeContextType } from "../../context/ThemeContext";
-const backendUrl = import.meta.env.VITE_BACKEND_URL;
+const backendUrl = import.meta.env.VITE_BACKEND_URL || 'http://localhost:5000';
interface LoginFormData {
email: string;
@@ -30,7 +30,9 @@ const Login: React.FC = () => {
setIsLoading(true);
try {
- const response = await axios.post(`${backendUrl}/api/auth/login`, formData);
+ const response = await axios.post(`${backendUrl}/api/auth/login`, formData, {
+ withCredentials: true,
+ });
setMessage(response.data.message);
if (response.data.message === 'Login successful') {
@@ -49,11 +51,10 @@ const Login: React.FC = () => {
return (
{/* Animated background elements */}
@@ -70,11 +71,10 @@ const Login: React.FC = () => {
-
+ }`}>
GitHubTracker
@@ -98,11 +98,10 @@ const Login: React.FC = () => {
onChange={handleChange}
autoComplete="username"
required
- className={`w-full pl-4 pr-4 py-4 rounded-2xl focus:outline-none transition-all ${
- mode === "dark"
+ className={`w-full pl-4 pr-4 py-4 rounded-2xl focus:outline-none transition-all ${mode === "dark"
? "bg-white/5 border border-white/10 text-white placeholder-slate-400 focus:ring-2 focus:ring-purple-500"
: "bg-gray-100 border border-gray-300 text-gray-900 placeholder-gray-500 focus:ring-2 focus:ring-purple-400"
- }`}
+ }`}
/>
@@ -115,11 +114,10 @@ const Login: React.FC = () => {
value={formData.password}
onChange={handleChange}
required
- className={`w-full pl-4 pr-4 py-4 rounded-2xl focus:outline-none transition-all ${
- mode === "dark"
+ className={`w-full pl-4 pr-4 py-4 rounded-2xl focus:outline-none transition-all ${mode === "dark"
? "bg-white/5 border border-white/10 text-white placeholder-slate-400 focus:ring-2 focus:ring-purple-500"
: "bg-gray-100 border border-gray-300 text-gray-900 placeholder-gray-500 focus:ring-2 focus:ring-purple-400"
- }`}
+ }`}
/>
@@ -134,11 +132,10 @@ const Login: React.FC = () => {
{/* Message */}
{message && (
-
+ }`}>
{message}
)}
diff --git a/src/pages/Signup/Signup.tsx b/src/pages/Signup/Signup.tsx
index 2ac51dcc..76957683 100644
--- a/src/pages/Signup/Signup.tsx
+++ b/src/pages/Signup/Signup.tsx
@@ -6,7 +6,7 @@ import { User, Mail, Lock, Eye, EyeOff } from "lucide-react";
import { ThemeContext } from "../../context/ThemeContext";
import type { ThemeContextType } from "../../context/ThemeContext";
-const backendUrl = import.meta.env.VITE_BACKEND_URL;
+const backendUrl = import.meta.env.VITE_BACKEND_URL || 'http://localhost:5000';
interface SignUpFormData {
username: string;
@@ -39,8 +39,8 @@ const SignUp: React.FC = () => {
if (name === "username") {
if (!value.trim()) {
errorMessage = "Username is required";
- } else if (!/^[A-Za-z\s]+$/.test(value)) {
- errorMessage = "Only letters are allowed";
+ } else if (!/^[a-zA-Z0-9_]+$/.test(value)) {
+ errorMessage = "Username can contain letters, numbers and underscores only";
}
}
if (name === "email") {
@@ -53,8 +53,8 @@ const SignUp: React.FC = () => {
if (name === "password") {
if (!value.trim()) {
errorMessage = "Password is required";
- } else if (!/^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d@$!%*#?&]{8,}$/.test(value)) {
- errorMessage = "Password must be 8+ characters with letters and numbers";
+ } else if (!/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/.test(value)) {
+ errorMessage = "Password must be 8+ chars and include uppercase, lowercase, number, and special char";
}
}
setErrors((prev) => ({ ...prev, [name]: errorMessage }));
@@ -64,28 +64,28 @@ const SignUp: React.FC = () => {
e.preventDefault();
const usernameError = !formData.username.trim()
? "Username is required"
- : !/^[A-Za-z\s]+$/.test(formData.username)
- ? "Only letters are allowed"
- : "";
+ : !/^[a-zA-Z0-9_]+$/.test(formData.username)
+ ? "Username can contain letters, numbers and underscores only"
+ : "";
const emailError = !formData.email.trim()
? "Email is required"
: !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email.trim())
- ? "Enter a valid email"
- : "";
+ ? "Enter a valid email"
+ : "";
const passwordError = !formData.password.trim()
? "Password is required"
- : !/^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d@$!%*#?&]{8,}$/.test(formData.password)
- ? "Password must be 8+ characters with letters and numbers"
- : "";
+ : !/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/.test(formData.password)
+ ? "Password must be 8+ chars and include uppercase, lowercase, number, and special char"
+ : "";
if (usernameError || emailError || passwordError) {
setErrors({ username: usernameError, email: emailError, password: passwordError });
return;
}
setIsLoading(true);
try {
- const response = await axios.post(`${backendUrl}/api/auth/signup`,
- formData // Include cookies for session
- );
+ const response = await axios.post(`${backendUrl}/api/auth/signup`, formData, {
+ withCredentials: true,
+ });
setMessage(response.data.message); // Show success message from backend
// Navigate to login page after successful signup
@@ -101,32 +101,27 @@ const SignUp: React.FC = () => {
return (
@@ -148,17 +143,15 @@ const SignUp: React.FC = () => {
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
- className={`rounded-3xl p-6 sm:p-10 shadow-2xl border ${
- mode === "dark"
- ? "bg-white/10 backdrop-blur-xl border-white/20 text-white"
- : "bg-white border-gray-200 text-black"
- }`}
+ className={`rounded-3xl p-6 sm:p-10 shadow-2xl border ${mode === "dark"
+ ? "bg-white/10 backdrop-blur-xl border-white/20 text-white"
+ : "bg-white border-gray-200 text-black"
+ }`}
>
-
+
Create Account
@@ -196,7 +189,7 @@ const SignUp: React.FC = () => {
-
setShowPassword(!showPassword)} aria-label={showPassword ? "Hide password" : "Show password"} aria-pressed={showPassword}
+ setShowPassword(!showPassword)} aria-label={showPassword ? "Hide password" : "Show password"}
className={`absolute inset-y-0 right-0 pr-4 flex items-center transition-colors duration-200 ${mode === "dark" ? "text-slate-400 hover:text-white" : "text-gray-500 hover:text-gray-800"}`}>
{showPassword ? : }
@@ -231,9 +224,8 @@ const SignUp: React.FC = () => {
);