Skip to content

Commit 6cf88b1

Browse files
merge aj -> phillip. implements authentication/chat/friends
1 parent 647b108 commit 6cf88b1

59 files changed

Lines changed: 1045 additions & 164 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

client/package-lock.json

Lines changed: 52 additions & 41 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@
2626
"class-variance-authority": "^0.7.1",
2727
"clsx": "^2.1.1",
2828
"is-inside-container": "^1.0.0",
29+
"jwt-decode": "^4.0.0",
2930
"lucide-react": "^0.516.0",
30-
"next": "15.4.7",
31+
"next": "^15.5.9",
3132
"react": "19.1.0",
3233
"react-dom": "19.1.0",
3334
"tailwind-merge": "^3.3.1",

client/src/components/Layout.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import Navbar from "@/components/ui/Navbar";
2+
3+
type LayoutProps = { children: React.ReactNode };
4+
5+
export default function Layout({ children }: LayoutProps) {
6+
return (
7+
<div className="flex min-h-screen flex-col">
8+
<Navbar />
9+
<main className="flex-1 p-4">{children}</main>
10+
<footer className="bg-gray-200 p-4 text-center">© 2026 My App</footer>
11+
</div>
12+
);
13+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import Link from "next/link";
2+
3+
import { useAuth } from "@/context/AuthContext";
4+
5+
const Navbar = () => {
6+
const { isLoggedIn, logout } = useAuth();
7+
8+
return (
9+
<nav className="flex justify-between bg-gray-200 p-4">
10+
<div className="flex gap-4">
11+
<Link href="/">Home</Link>
12+
<Link href="/chat">Chat</Link>
13+
</div>
14+
15+
<div className="flex gap-4">
16+
{isLoggedIn ? (
17+
<>
18+
<button onClick={logout} className="text-red-500">
19+
Logout
20+
</button>
21+
</>
22+
) : (
23+
<>
24+
<Link href="/login">Login</Link>
25+
<Link href="/signup">Signup</Link>
26+
</>
27+
)}
28+
</div>
29+
</nav>
30+
);
31+
};
32+
33+
export default Navbar;

client/src/context/AuthContext.tsx

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { useRouter } from "next/router";
2+
import {
3+
createContext,
4+
ReactNode,
5+
useContext,
6+
useEffect,
7+
useState,
8+
} from "react";
9+
10+
type AuthContextType = {
11+
accessToken: string | null;
12+
refreshToken: string | null;
13+
login: (access: string, refresh: string) => void;
14+
logout: () => void;
15+
isLoggedIn: boolean;
16+
};
17+
18+
const AuthContext = createContext<AuthContextType | undefined>(undefined);
19+
20+
export const AuthProvider = ({ children }: { children: ReactNode }) => {
21+
const [accessToken, setAccessToken] = useState<string | null>(null);
22+
const [refreshToken, setRefreshToken] = useState<string | null>(null);
23+
const router = useRouter();
24+
25+
useEffect(() => {
26+
const storedAccess = localStorage.getItem("accessToken");
27+
const storedRefresh = localStorage.getItem("refreshToken");
28+
if (storedAccess) setAccessToken(storedAccess);
29+
if (storedRefresh) setRefreshToken(storedRefresh);
30+
}, []);
31+
32+
const login = (access: string, refresh: string) => {
33+
setAccessToken(access);
34+
setRefreshToken(refresh);
35+
localStorage.setItem("accessToken", access);
36+
localStorage.setItem("refreshToken", refresh);
37+
};
38+
39+
const logout = () => {
40+
setAccessToken(null);
41+
setRefreshToken(null);
42+
localStorage.removeItem("accessToken");
43+
localStorage.removeItem("refreshToken");
44+
router.push("/login");
45+
};
46+
47+
const isLoggedIn = !!accessToken;
48+
49+
return (
50+
<AuthContext.Provider
51+
value={{ accessToken, refreshToken, login, logout, isLoggedIn }}
52+
>
53+
{children}
54+
</AuthContext.Provider>
55+
);
56+
};
57+
58+
export const useAuth = () => {
59+
const context = useContext(AuthContext);
60+
if (!context) throw new Error("useAuth must be used within AuthProvider");
61+
return context;
62+
};

client/src/pages/_app.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,17 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
44
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
55
import type { AppProps } from "next/app";
66

7+
import { AuthProvider } from "@/context/AuthContext";
8+
79
const queryClient = new QueryClient();
810

911
export default function App({ Component, pageProps }: AppProps) {
1012
return (
11-
<QueryClientProvider client={queryClient}>
12-
<ReactQueryDevtools initialIsOpen={false} />
13-
<Component {...pageProps} />
14-
</QueryClientProvider>
13+
<AuthProvider>
14+
<QueryClientProvider client={queryClient}>
15+
<ReactQueryDevtools initialIsOpen={false} />
16+
<Component {...pageProps} />
17+
</QueryClientProvider>
18+
</AuthProvider>
1519
);
1620
}

client/src/pages/add-friend.tsx

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { useState } from "react";
2+
3+
import Layout from "@/components/Layout";
4+
import { useAuth } from "@/context/AuthContext";
5+
6+
export default function AddFriend() {
7+
const { accessToken } = useAuth();
8+
const [username, setUsername] = useState("");
9+
const [message, setMessage] = useState("");
10+
11+
const handleAdd = async () => {
12+
const res = await fetch("http://localhost:8000/api/chat/add_friend/", {
13+
method: "POST",
14+
headers: {
15+
"Content-Type": "application/json",
16+
Authorization: `Bearer ${accessToken}`,
17+
},
18+
body: JSON.stringify({ username }),
19+
});
20+
const data = await res.json();
21+
setMessage(data.detail || "Error");
22+
};
23+
24+
return (
25+
<Layout>
26+
<div className="mx-auto max-w-md p-4">
27+
<h1 className="mb-2 text-xl font-bold">Add Friend</h1>
28+
<input
29+
type="text"
30+
value={username}
31+
onChange={(e) => setUsername(e.target.value)}
32+
placeholder="Enter username"
33+
className="mb-2 w-full border p-2"
34+
/>
35+
<button
36+
onClick={handleAdd}
37+
className="w-full rounded bg-green-500 p-2 text-white"
38+
>
39+
Add Friend
40+
</button>
41+
{message && <p className="mt-2">{message}</p>}
42+
</div>
43+
</Layout>
44+
);
45+
}

0 commit comments

Comments
 (0)