From 90e6a85fffa34aa480019d05e4051d66d862670e Mon Sep 17 00:00:00 2001 From: Vishesh 'ironeagle' Bangotra Date: Tue, 11 Nov 2025 15:45:24 +0530 Subject: [PATCH 1/9] jwt provider and common api utils --- src/blog/providers/Article.tsx | 22 +++++++++++----------- src/blog/providers/Author.tsx | 23 ++++++++--------------- src/blog/utils/api.ts | 33 +++++++++++++++++++++++++++++++++ src/main.jsx | 9 ++++++--- 4 files changed, 58 insertions(+), 29 deletions(-) create mode 100644 src/blog/utils/api.ts diff --git a/src/blog/providers/Article.tsx b/src/blog/providers/Article.tsx index 4ecc5ef..a66cf0f 100644 --- a/src/blog/providers/Article.tsx +++ b/src/blog/providers/Article.tsx @@ -1,28 +1,27 @@ import React, { createContext, useState, useContext, useEffect } from 'react'; -import axios from 'axios'; -import { ArticleModel } from "../types/models"; -import { ArticleContextModel } from "../types/contexts"; +import { api } from '../utils/api'; +import { ArticleModel } from '../types/models'; +import { ArticleContextModel } from '../types/contexts'; +import { useAuth } from './Author'; const ArticleContext = createContext(undefined); -const API_BASE = import.meta.env.VITE_API_BASE_URL; - export const ArticleProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [articles, setArticles] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const { token } = useAuth(); // βœ… access token if needed + /** πŸ”Ή Fetch articles (JWT automatically attached by api.ts interceptor) */ const fetchArticles = async () => { try { setLoading(true); setError(null); - // βœ… Use correct full endpoint from OpenAPI spec - const res = await axios.get(`${API_BASE}/articles`, { + const res = await api.get('/articles', { params: { skip: 0, limit: 10 }, }); - // βœ… Normalize if backend sends _id instead of id const formatted = res.data.map((a) => ({ ...a, id: a._id || undefined, @@ -31,15 +30,16 @@ export const ArticleProvider: React.FC<{ children: React.ReactNode }> = ({ child setArticles(formatted); } catch (err: any) { console.error('Failed to fetch articles:', err); - setError(err.message || 'Failed to fetch articles'); + setError(err.response?.data?.detail || 'Failed to fetch articles'); } finally { setLoading(false); } }; + /** πŸ”Ή Auto-fetch articles whenever user logs in/out */ useEffect(() => { - fetchArticles(); - }, []); + if (token) fetchArticles(); + }, [token]); return ( diff --git a/src/blog/providers/Author.tsx b/src/blog/providers/Author.tsx index 4cd8ac6..ed48105 100644 --- a/src/blog/providers/Author.tsx +++ b/src/blog/providers/Author.tsx @@ -1,12 +1,10 @@ import React, { createContext, useState, useEffect, useContext } from 'react'; -import axios from 'axios'; -import { AuthorModel } from "../types/models"; -import { AuthContextModel } from "../types/contexts"; +import { api } from '../utils/api'; +import { AuthorModel } from '../types/models'; +import { AuthContextModel } from '../types/contexts'; const AuthContext = createContext(undefined); -const API_BASE = import.meta.env.VITE_API_BASE_URL; - export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [currentUser, setCurrentUser] = useState(null); const [authors, setAuthors] = useState([]); @@ -20,7 +18,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children setLoading(true); setError(null); - const res = await axios.post(`${API_BASE}/auth/login`, { email, password }); + const res = await api.post('/auth/login', { email, password }); const { access_token, user } = res.data; if (access_token) { @@ -44,17 +42,13 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children setAuthors([]); }; - /** πŸ”Ή Fetch all authors (requires valid JWT) */ + /** πŸ”Ή Fetch all authors (JWT handled by api interceptor) */ const refreshAuthors = async () => { - if (!token) return; try { setLoading(true); setError(null); - const res = await axios.get(`${API_BASE}/authors`, { - headers: { Authorization: `Bearer ${token}` }, - }); - + const res = await api.get('/authors'); setAuthors(res.data); } catch (err: any) { console.error('Failed to fetch authors:', err); @@ -68,9 +62,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children const fetchCurrentUser = async () => { if (!token) return; try { - const res = await axios.get(`${API_BASE}/auth/me`, { - headers: { Authorization: `Bearer ${token}` }, - }); + const res = await api.get('/auth/me'); setCurrentUser(res.data); } catch (err: any) { console.error('Failed to fetch current user:', err); @@ -78,6 +70,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children } }; + /** πŸ”Ή On mount, try to fetch user if token exists */ useEffect(() => { if (token) fetchCurrentUser(); }, [token]); diff --git a/src/blog/utils/api.ts b/src/blog/utils/api.ts new file mode 100644 index 0000000..671417f --- /dev/null +++ b/src/blog/utils/api.ts @@ -0,0 +1,33 @@ +// src/utils/api.ts +import axios from 'axios'; + +const API_BASE = import.meta.env.VITE_API_BASE_URL; + +export const api = axios.create({ + baseURL: API_BASE, + headers: { + 'Content-Type': 'application/json', + }, +}); + +// πŸ”Ή Attach token from localStorage before each request +api.interceptors.request.use((config) => { + const token = localStorage.getItem('token'); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; +}); + +// πŸ”Ή Handle expired or invalid tokens globally +api.interceptors.response.use( + (response) => response, + (error) => { + if (error.response?.status === 401) { + console.warn('Token expired or invalid. Logging out...'); + localStorage.removeItem('token'); + // Optionally: trigger a redirect or event + } + return Promise.reject(error); + } +); diff --git a/src/main.jsx b/src/main.jsx index 2ef2626..d93e38c 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -2,14 +2,17 @@ import * as React from 'react'; import { createRoot } from 'react-dom/client'; import Blog from './blog/Blog'; import { ArticleProvider } from './blog/providers/Article'; +import { AuthProvider } from './blog/providers/Author'; const rootElement = document.getElementById('root'); const root = createRoot(rootElement); root.render( - - - + + + + + , ); From 3bf0a5839cde3ca159208840186b8200e04d108f Mon Sep 17 00:00:00 2001 From: Vishesh 'ironeagle' Bangotra Date: Tue, 11 Nov 2025 18:33:40 +0530 Subject: [PATCH 2/9] register function in Author contexts --- src/blog/providers/Author.tsx | 19 ++++++++++++++++++- src/blog/types/contexts.ts | 1 + 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/blog/providers/Author.tsx b/src/blog/providers/Author.tsx index ed48105..99e09a2 100644 --- a/src/blog/providers/Author.tsx +++ b/src/blog/providers/Author.tsx @@ -12,6 +12,22 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children const [loading, setLoading] = useState(false); const [error, setError] = useState(null); + /** πŸ”Ή Register new user */ + const register = async (username: string, email: string, password: string) => { + try { + setLoading(true); + setError(null); + + const res = await api.post('/auth/register', { username, email, password }); + return res.data; // returns PublicUser from backend + } catch (err: any) { + console.error('Registration failed:', err); + setError(err.response?.data?.detail || 'Registration failed'); + } finally { + setLoading(false); + } + }; + /** πŸ”Ή Login and store JWT token */ const login = async (email: string, password: string) => { try { @@ -52,7 +68,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children setAuthors(res.data); } catch (err: any) { console.error('Failed to fetch authors:', err); - setError(err.message || 'Failed to fetch authors'); + setError(err.response?.data?.detail || 'Failed to fetch authors'); } finally { setLoading(false); } @@ -85,6 +101,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children error, login, logout, + register, refreshAuthors, }} > diff --git a/src/blog/types/contexts.ts b/src/blog/types/contexts.ts index 2f12ed6..e9f3f7e 100644 --- a/src/blog/types/contexts.ts +++ b/src/blog/types/contexts.ts @@ -14,6 +14,7 @@ export interface AuthContextModel { loading: boolean; error: string | null; login: (email: string, password: string) => Promise; + register: (username: string, email: string, password: string) => Promise; logout: () => void; refreshAuthors: () => Promise; } From b2a7df5760c8281130640985919819f3fd6f2368 Mon Sep 17 00:00:00 2001 From: Vishesh 'ironeagle' Bangotra Date: Tue, 11 Nov 2025 18:47:16 +0530 Subject: [PATCH 3/9] username and password instead of email and password --- src/blog/providers/Author.tsx | 8 ++++---- src/blog/types/contexts.ts | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/blog/providers/Author.tsx b/src/blog/providers/Author.tsx index 99e09a2..b212a5a 100644 --- a/src/blog/providers/Author.tsx +++ b/src/blog/providers/Author.tsx @@ -13,12 +13,12 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children const [error, setError] = useState(null); /** πŸ”Ή Register new user */ - const register = async (username: string, email: string, password: string) => { + const register = async (username: string, password: string) => { try { setLoading(true); setError(null); - const res = await api.post('/auth/register', { username, email, password }); + const res = await api.post('/auth/register', { username, password }); return res.data; // returns PublicUser from backend } catch (err: any) { console.error('Registration failed:', err); @@ -29,12 +29,12 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }; /** πŸ”Ή Login and store JWT token */ - const login = async (email: string, password: string) => { + const login = async (username: string, password: string) => { try { setLoading(true); setError(null); - const res = await api.post('/auth/login', { email, password }); + const res = await api.post('/auth/login', { username, password }); const { access_token, user } = res.data; if (access_token) { diff --git a/src/blog/types/contexts.ts b/src/blog/types/contexts.ts index e9f3f7e..2100ccd 100644 --- a/src/blog/types/contexts.ts +++ b/src/blog/types/contexts.ts @@ -13,8 +13,8 @@ export interface AuthContextModel { token: string | null; loading: boolean; error: string | null; - login: (email: string, password: string) => Promise; - register: (username: string, email: string, password: string) => Promise; + login: (username: string, password: string) => Promise; + register: (username: string, password: string) => Promise; logout: () => void; refreshAuthors: () => Promise; } From 661f8c915beea678213c58d8f9a587779455bd27 Mon Sep 17 00:00:00 2001 From: Vishesh 'ironeagle' Bangotra Date: Tue, 11 Nov 2025 18:47:49 +0530 Subject: [PATCH 4/9] fixes for public listed articles --- src/blog/providers/Article.tsx | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/blog/providers/Article.tsx b/src/blog/providers/Article.tsx index a66cf0f..9809215 100644 --- a/src/blog/providers/Article.tsx +++ b/src/blog/providers/Article.tsx @@ -18,15 +18,8 @@ export const ArticleProvider: React.FC<{ children: React.ReactNode }> = ({ child setLoading(true); setError(null); - const res = await api.get('/articles', { - params: { skip: 0, limit: 10 }, - }); - - const formatted = res.data.map((a) => ({ - ...a, - id: a._id || undefined, - })); - + const res = await api.get('/articles', { params: { skip: 0, limit: 10 } }); + const formatted = res.data.map((a) => ({ ...a, id: a._id || undefined })); setArticles(formatted); } catch (err: any) { console.error('Failed to fetch articles:', err); @@ -38,7 +31,13 @@ export const ArticleProvider: React.FC<{ children: React.ReactNode }> = ({ child /** πŸ”Ή Auto-fetch articles whenever user logs in/out */ useEffect(() => { - if (token) fetchArticles(); + // Always load once on mount + // If endpoint requires JWT, fallback safely + if (!token) { + fetchArticles().catch(() => setLoading(false)); // try anyway (handles both public/protected) + } else { + fetchArticles(); + } }, [token]); return ( From 1c964a7fee90849d05bec7c46253dff3d80f2296 Mon Sep 17 00:00:00 2001 From: Vishesh 'ironeagle' Bangotra Date: Tue, 11 Nov 2025 18:47:59 +0530 Subject: [PATCH 5/9] login page --- src/blog/Blog.tsx | 46 +++++++++++------- src/blog/components/Login.tsx | 89 +++++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 17 deletions(-) create mode 100644 src/blog/components/Login.tsx diff --git a/src/blog/Blog.tsx b/src/blog/Blog.tsx index 7bd502b..d3b3e9c 100644 --- a/src/blog/Blog.tsx +++ b/src/blog/Blog.tsx @@ -2,16 +2,22 @@ import * as React from 'react'; import CssBaseline from '@mui/material/CssBaseline'; import Container from '@mui/material/Container'; import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; import AppTheme from '../shared-theme/AppTheme'; import MainContent from './components/MainContent'; import Article from './components/Article'; import Latest from './components/Latest'; import Footer from './components/Footer'; +import Login from './components/Login'; import { useArticles } from './providers/Article'; +import { useAuth } from './providers/Author'; export default function Blog(props: { disableCustomTheme?: boolean }) { const { articles, loading, error } = useArticles(); + const { currentUser } = useAuth(); + const [selectedArticle, setSelectedArticle] = React.useState(null); + const [showLogin, setShowLogin] = React.useState(false); const handleSelectArticle = (index: number) => { setSelectedArticle(index); @@ -20,6 +26,13 @@ export default function Blog(props: { disableCustomTheme?: boolean }) { const handleBack = () => setSelectedArticle(null); + const handleShowLogin = () => { + setShowLogin(true); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }; + + const handleHideLogin = () => setShowLogin(false); + if (loading) { return ( @@ -64,13 +77,7 @@ export default function Blog(props: { disableCustomTheme?: boolean }) { - + - {selectedArticle === null ? ( + {showLogin ? ( + + ) : selectedArticle === null ? ( <> - + {!currentUser && ( + + + + )} + + { - // Optional pagination call - }} + onLoadMore={async () => {}} /> ) : ( @@ -102,7 +114,7 @@ export default function Blog(props: { disableCustomTheme?: boolean }) { )} - {selectedArticle === null && ( + {selectedArticle === null && !showLogin && ( void; +} + +export default function Login({ onBack }: LoginProps) { + const { login, loading, error, currentUser } = useAuth(); + const [username, setUsername] = React.useState(''); + const [password, setPassword] = React.useState(''); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + await login(username, password); + }; + + if (currentUser) { + // βœ… if logged in, auto-return to the article list + onBack(); + return null; + } + + return ( + + + + + + + Sign In + + + + Please log in to continue + + +
+ setUsername(e.target.value)} + required + /> + setPassword(e.target.value)} + required + /> + + {error && ( + + {error} + + )} + + + +
+ ); +} From 0267aedf5218a5f769a06de2e918c09d8e701f4c Mon Sep 17 00:00:00 2001 From: Vishesh 'ironeagle' Bangotra Date: Tue, 11 Nov 2025 18:48:06 +0530 Subject: [PATCH 6/9] register page --- src/blog/components/Register.tsx | 114 +++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 src/blog/components/Register.tsx diff --git a/src/blog/components/Register.tsx b/src/blog/components/Register.tsx new file mode 100644 index 0000000..9359516 --- /dev/null +++ b/src/blog/components/Register.tsx @@ -0,0 +1,114 @@ +import * as React from 'react'; +import { Box, TextField, Button, Typography, IconButton, CircularProgress, Alert, } from '@mui/material'; +import ArrowBackRoundedIcon from '@mui/icons-material/ArrowBackRounded'; +import { useAuth } from '../providers/Author'; + +interface RegisterProps { + onBack: () => void; +} + +export default function Register({ onBack }: RegisterProps) { + const { register, loading, error, currentUser } = useAuth(); + const [username, setUsername] = React.useState(''); + const [password1, setPassword1] = React.useState(''); + const [password2, setPassword2] = React.useState(''); + const [localError, setLocalError] = React.useState(null); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLocalError(null); + + // βœ… Local validation + if (password1 !== password2) { + setLocalError("Passwords don't match"); + return; + } + + if (password1.length < 6) { + setLocalError('Password must be at least 6 characters long'); + return; + } + + // βœ… Call backend + await register(username, password1); + }; + + if (currentUser) { + // βœ… if logged in, auto-return to the article list + onBack(); + return null; + } + + return ( + + + + + + + Sign Up + + + + Please sign up to continue + + +
+ setUsername(e.target.value)} + required + /> + setPassword1(e.target.value)} + required + /> + setPassword2(e.target.value)} + required + /> + + {(localError || error) && ( + + {localError || error} + + )} + + + +
+ ); +} From 557e8ddfc9d4276ed449c5f6afddf487d0b3aab0 Mon Sep 17 00:00:00 2001 From: Vishesh 'ironeagle' Bangotra Date: Tue, 11 Nov 2025 18:56:48 +0530 Subject: [PATCH 7/9] working login and register page --- src/blog/Blog.tsx | 33 ++++++++++++++++++++++++++------- src/blog/components/Login.tsx | 32 +++++++++++++++++++++++++------- 2 files changed, 51 insertions(+), 14 deletions(-) diff --git a/src/blog/Blog.tsx b/src/blog/Blog.tsx index d3b3e9c..a98268a 100644 --- a/src/blog/Blog.tsx +++ b/src/blog/Blog.tsx @@ -9,6 +9,7 @@ import Article from './components/Article'; import Latest from './components/Latest'; import Footer from './components/Footer'; import Login from './components/Login'; +import Register from './components/Register'; import { useArticles } from './providers/Article'; import { useAuth } from './providers/Author'; @@ -18,6 +19,7 @@ export default function Blog(props: { disableCustomTheme?: boolean }) { const [selectedArticle, setSelectedArticle] = React.useState(null); const [showLogin, setShowLogin] = React.useState(false); + const [showRegister, setShowRegister] = React.useState(false); const handleSelectArticle = (index: number) => { setSelectedArticle(index); @@ -87,16 +89,35 @@ export default function Blog(props: { disableCustomTheme?: boolean }) { flexDirection: 'column', my: 4, gap: 4, - pb: selectedArticle === null && !showLogin ? 24 : 0, + pb: selectedArticle === null && !showLogin && !showRegister ? 24 : 0, // βœ… adjusted footer padding }} > - {showLogin ? ( - - ) : selectedArticle === null ? ( + {showRegister ? ( + { + setShowRegister(false); + setShowLogin(false); + }} + /> + ) : showLogin ? ( + setShowLogin(false)} + onRegister={() => { + setShowLogin(false); + setShowRegister(true); + }} + /> + ) : selectedArticle !== null ? ( +
+ ) : ( <> {!currentUser && ( - @@ -109,8 +130,6 @@ export default function Blog(props: { disableCustomTheme?: boolean }) { onLoadMore={async () => {}} /> - ) : ( -
)} diff --git a/src/blog/components/Login.tsx b/src/blog/components/Login.tsx index a601752..3c04765 100644 --- a/src/blog/components/Login.tsx +++ b/src/blog/components/Login.tsx @@ -1,13 +1,14 @@ import * as React from 'react'; -import { Box, TextField, Button, Typography, IconButton, CircularProgress } from '@mui/material'; +import { Box, TextField, Button, Typography, IconButton, CircularProgress, Link } from '@mui/material'; import ArrowBackRoundedIcon from '@mui/icons-material/ArrowBackRounded'; import { useAuth } from '../providers/Author'; interface LoginProps { onBack: () => void; + onRegister: () => void; } -export default function Login({ onBack }: LoginProps) { +export default function Login({ onBack, onRegister }: LoginProps) { const { login, loading, error, currentUser } = useAuth(); const [username, setUsername] = React.useState(''); const [password, setPassword] = React.useState(''); @@ -17,11 +18,10 @@ export default function Login({ onBack }: LoginProps) { await login(username, password); }; - if (currentUser) { - // βœ… if logged in, auto-return to the article list - onBack(); - return null; - } + // βœ… Auto-return if already logged in + React.useEffect(() => { + if (currentUser) onBack(); + }, [currentUser]); return ( : 'Login'} + + + Don’t have an account?{' '} + + Register + + ); } From 89aa1c6ce4ad9ae8cbeffaf8189d6ef099a0fc0c Mon Sep 17 00:00:00 2001 From: Vishesh 'ironeagle' Bangotra Date: Tue, 11 Nov 2025 19:10:02 +0530 Subject: [PATCH 8/9] cleanup code for view --- src/blog/Blog.tsx | 107 ++++++++++++++++++++++++++++------------------ 1 file changed, 65 insertions(+), 42 deletions(-) diff --git a/src/blog/Blog.tsx b/src/blog/Blog.tsx index a98268a..e189fa1 100644 --- a/src/blog/Blog.tsx +++ b/src/blog/Blog.tsx @@ -13,6 +13,8 @@ import Register from './components/Register'; import { useArticles } from './providers/Article'; import { useAuth } from './providers/Author'; +type View = 'home' | 'login' | 'register' | 'article'; + export default function Blog(props: { disableCustomTheme?: boolean }) { const { articles, loading, error } = useArticles(); const { currentUser } = useAuth(); @@ -30,10 +32,69 @@ export default function Blog(props: { disableCustomTheme?: boolean }) { const handleShowLogin = () => { setShowLogin(true); + setShowRegister(false); window.scrollTo({ top: 0, behavior: 'smooth' }); }; + const handleShowRegister = () => { + setShowRegister(true); + setShowLogin(false); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }; + const handleHideAuth = () => { + setShowLogin(false); + setShowRegister(false); + }; - const handleHideLogin = () => setShowLogin(false); + // derive a single source of truth for view + const view: View = React.useMemo(() => { + if (selectedArticle !== null) return 'article'; + if (showRegister) return 'register'; + if (showLogin) return 'login'; + return 'home'; + }, [selectedArticle, showLogin, showRegister]); + + // render function keeps JSX tidy + const renderView = () => { + switch (view) { + case 'register': + return ; + case 'login': + return ( + { + handleShowRegister(); + }} + /> + ); + case 'article': + if (selectedArticle == null || !articles[selectedArticle]) return null; + return
; + case 'home': + default: + return ( + <> + {!currentUser && ( + + + + )} + + + {}} + /> + + ); + } + }; if (loading) { return ( @@ -89,51 +150,13 @@ export default function Blog(props: { disableCustomTheme?: boolean }) { flexDirection: 'column', my: 4, gap: 4, - pb: selectedArticle === null && !showLogin && !showRegister ? 24 : 0, // βœ… adjusted footer padding + pb: view === 'home' ? 24 : 0, }} > - {showRegister ? ( - { - setShowRegister(false); - setShowLogin(false); - }} - /> - ) : showLogin ? ( - setShowLogin(false)} - onRegister={() => { - setShowLogin(false); - setShowRegister(true); - }} - /> - ) : selectedArticle !== null ? ( -
- ) : ( - <> - {!currentUser && ( - - - - )} - - - {}} - /> - - )} + {renderView()} - {selectedArticle === null && !showLogin && ( + {view === 'home' && ( Date: Tue, 11 Nov 2025 20:47:37 +0530 Subject: [PATCH 9/9] profile and update view for author --- src/blog/Blog.tsx | 53 +++++++--- src/blog/components/Profile.tsx | 166 ++++++++++++++++++++++++++++++++ src/blog/providers/Author.tsx | 34 ++++++- src/blog/types/contexts.ts | 1 + 4 files changed, 239 insertions(+), 15 deletions(-) create mode 100644 src/blog/components/Profile.tsx diff --git a/src/blog/Blog.tsx b/src/blog/Blog.tsx index e189fa1..397a3ad 100644 --- a/src/blog/Blog.tsx +++ b/src/blog/Blog.tsx @@ -10,10 +10,11 @@ import Latest from './components/Latest'; import Footer from './components/Footer'; import Login from './components/Login'; import Register from './components/Register'; +import Profile from './components/Profile'; import { useArticles } from './providers/Article'; import { useAuth } from './providers/Author'; -type View = 'home' | 'login' | 'register' | 'article'; +type View = 'home' | 'login' | 'register' | 'article' | 'profile'; export default function Blog(props: { disableCustomTheme?: boolean }) { const { articles, loading, error } = useArticles(); @@ -22,6 +23,7 @@ export default function Blog(props: { disableCustomTheme?: boolean }) { const [selectedArticle, setSelectedArticle] = React.useState(null); const [showLogin, setShowLogin] = React.useState(false); const [showRegister, setShowRegister] = React.useState(false); + const [showProfile, setShowProfile] = React.useState(false); const handleSelectArticle = (index: number) => { setSelectedArticle(index); @@ -44,14 +46,22 @@ export default function Blog(props: { disableCustomTheme?: boolean }) { setShowLogin(false); setShowRegister(false); }; + const handleShowProfile = () => { + setShowProfile(true); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }; + const handleHideProfile = () => { + setShowProfile(false); + }; // derive a single source of truth for view const view: View = React.useMemo(() => { if (selectedArticle !== null) return 'article'; if (showRegister) return 'register'; if (showLogin) return 'login'; + if (showProfile) return 'profile'; return 'home'; - }, [selectedArticle, showLogin, showRegister]); + }, [selectedArticle, showLogin, showRegister, showProfile]); // render function keeps JSX tidy const renderView = () => { @@ -67,6 +77,12 @@ export default function Blog(props: { disableCustomTheme?: boolean }) { }} /> ); + case 'profile': + return ( + + ); case 'article': if (selectedArticle == null || !articles[selectedArticle]) return null; return
; @@ -74,16 +90,29 @@ export default function Blog(props: { disableCustomTheme?: boolean }) { default: return ( <> - {!currentUser && ( - - - - )} + + {!currentUser ? ( + <> + + + ) : ( + <> + + + )} + void; +} + +export default function Profile({ onBack }: ProfileProps) { + const { currentUser, loading, error, token, refreshAuthors, updateProfile } = useAuth(); + const [formData, setFormData] = React.useState({ + username: currentUser?.username || '', + name: currentUser?.name || '', + email: currentUser?.email || '', + avatar: currentUser?.avatar || '', + }); + const [success, setSuccess] = React.useState(null); + const [saving, setSaving] = React.useState(false); + + React.useEffect(() => { + if (currentUser) setFormData(currentUser); + }, [currentUser]); + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData((prev) => ({ ...prev, [name]: value })); + }; + + const handleSave = async () => { + if (!currentUser) return; + + try { + setSaving(true); + setSuccess(null); + + const updatedUser = { ...currentUser, ...formData }; + console.log('updatedUser'); + console.log(updatedUser); + const updated = await updateProfile(updatedUser); + + if (updated) setSuccess('Profile updated successfully'); + } catch (err: any) { + console.error('Failed to update profile:', err); + } finally { + setSaving(false); + } + }; + + if (!currentUser) { + return ( + + + You must be logged in to view your profile. + + + + ); + } + + return ( + + + + + + + Profile + + + + + + + + + + + + {error && ( + + {error} + + )} + {success && ( + + {success} + + )} + + + + ); +} diff --git a/src/blog/providers/Author.tsx b/src/blog/providers/Author.tsx index b212a5a..e5d1dad 100644 --- a/src/blog/providers/Author.tsx +++ b/src/blog/providers/Author.tsx @@ -74,13 +74,40 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children } }; + /** πŸ”Ή Update current user (full model) */ + const updateProfile = async (userData: AuthorModel) => { + if (!userData._id) { + console.error('updateProfile called without _id'); + return; + } + try { + setLoading(true); + setError(null); + + const res = await api.put(`/authors/${userData._id}`, userData); + setCurrentUser(res.data); + return res.data; + } catch (err: any) { + console.error('Profile update failed:', err); + setError(err.response?.data?.detail || 'Failed to update profile'); + } finally { + setLoading(false); + } + }; + + /** πŸ”Ή Auto-load current user if token exists */ const fetchCurrentUser = async () => { if (!token) return; try { - const res = await api.get('/auth/me'); - setCurrentUser(res.data); - } catch (err: any) { + const me = await api.get<{ _id: string; username: string; email: string }>('/auth/me'); + + const author = await api.get(`/authors/${me.data._id}`); + + const fullUser = { ...me.data, ...author.data }; + + setCurrentUser(fullUser); + } catch (err) { console.error('Failed to fetch current user:', err); logout(); // invalid/expired token } @@ -103,6 +130,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children logout, register, refreshAuthors, + updateProfile, }} > {children} diff --git a/src/blog/types/contexts.ts b/src/blog/types/contexts.ts index 2100ccd..0aab367 100644 --- a/src/blog/types/contexts.ts +++ b/src/blog/types/contexts.ts @@ -17,4 +17,5 @@ export interface AuthContextModel { register: (username: string, password: string) => Promise; logout: () => void; refreshAuthors: () => Promise; + updateProfile: (user: AuthorModel) => Promise; }