profile and update view for author

This commit is contained in:
2025-11-11 20:47:37 +05:30
parent 89aa1c6ce4
commit 8a29261a3e
4 changed files with 239 additions and 15 deletions

View File

@@ -10,10 +10,11 @@ import Latest from './components/Latest';
import Footer from './components/Footer'; import Footer from './components/Footer';
import Login from './components/Login'; import Login from './components/Login';
import Register from './components/Register'; import Register from './components/Register';
import Profile from './components/Profile';
import { useArticles } from './providers/Article'; import { useArticles } from './providers/Article';
import { useAuth } from './providers/Author'; 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 }) { export default function Blog(props: { disableCustomTheme?: boolean }) {
const { articles, loading, error } = useArticles(); const { articles, loading, error } = useArticles();
@@ -22,6 +23,7 @@ export default function Blog(props: { disableCustomTheme?: boolean }) {
const [selectedArticle, setSelectedArticle] = React.useState<number | null>(null); const [selectedArticle, setSelectedArticle] = React.useState<number | null>(null);
const [showLogin, setShowLogin] = React.useState(false); const [showLogin, setShowLogin] = React.useState(false);
const [showRegister, setShowRegister] = React.useState(false); const [showRegister, setShowRegister] = React.useState(false);
const [showProfile, setShowProfile] = React.useState(false);
const handleSelectArticle = (index: number) => { const handleSelectArticle = (index: number) => {
setSelectedArticle(index); setSelectedArticle(index);
@@ -44,14 +46,22 @@ export default function Blog(props: { disableCustomTheme?: boolean }) {
setShowLogin(false); setShowLogin(false);
setShowRegister(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 // derive a single source of truth for view
const view: View = React.useMemo(() => { const view: View = React.useMemo(() => {
if (selectedArticle !== null) return 'article'; if (selectedArticle !== null) return 'article';
if (showRegister) return 'register'; if (showRegister) return 'register';
if (showLogin) return 'login'; if (showLogin) return 'login';
if (showProfile) return 'profile';
return 'home'; return 'home';
}, [selectedArticle, showLogin, showRegister]); }, [selectedArticle, showLogin, showRegister, showProfile]);
// render function keeps JSX tidy // render function keeps JSX tidy
const renderView = () => { const renderView = () => {
@@ -67,6 +77,12 @@ export default function Blog(props: { disableCustomTheme?: boolean }) {
}} }}
/> />
); );
case 'profile':
return (
<Profile
onBack={handleHideProfile}
/>
);
case 'article': case 'article':
if (selectedArticle == null || !articles[selectedArticle]) return null; if (selectedArticle == null || !articles[selectedArticle]) return null;
return <Article article={articles[selectedArticle]} onBack={handleBack} />; return <Article article={articles[selectedArticle]} onBack={handleBack} />;
@@ -74,16 +90,29 @@ export default function Blog(props: { disableCustomTheme?: boolean }) {
default: default:
return ( return (
<> <>
{!currentUser && ( <Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2, gap: 1 }}>
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}> {!currentUser ? (
<>
<Button <Button
variant="outlined" variant="outlined"
color="primary" color="primary"
onClick={handleShowLogin}> onClick={handleShowLogin}
>
Login Login
</Button> </Button>
</Box> </>
) : (
<>
<Button
variant="outlined"
color="primary"
onClick={() => setShowProfile(true)}
>
Profile
</Button>
</>
)} )}
</Box>
<MainContent articles={articles} onSelectArticle={handleSelectArticle} /> <MainContent articles={articles} onSelectArticle={handleSelectArticle} />
<Latest <Latest

View File

@@ -0,0 +1,166 @@
import * as React from 'react';
import {
Box,
TextField,
Button,
Typography,
IconButton,
CircularProgress,
Avatar,
Alert,
} from '@mui/material';
import ArrowBackRoundedIcon from '@mui/icons-material/ArrowBackRounded';
import { useAuth } from '../providers/Author';
interface ProfileProps {
onBack: () => 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<string | null>(null);
const [saving, setSaving] = React.useState(false);
React.useEffect(() => {
if (currentUser) setFormData(currentUser);
}, [currentUser]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
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 (
<Box
sx={{
maxWidth: 400,
mx: 'auto',
mt: 8,
p: 4,
borderRadius: 3,
boxShadow: 3,
bgcolor: 'background.paper',
}}
>
<Typography variant="h6" align="center">
You must be logged in to view your profile.
</Typography>
<Button fullWidth variant="outlined" sx={{ mt: 2 }} onClick={onBack}>
Back
</Button>
</Box>
);
}
return (
<Box
sx={{
maxWidth: 500,
mx: 'auto',
mt: 8,
p: 4,
borderRadius: 3,
boxShadow: 3,
bgcolor: 'background.paper',
}}
>
<IconButton onClick={onBack} sx={{ mb: 2 }}>
<ArrowBackRoundedIcon />
</IconButton>
<Typography variant="h4" fontWeight="bold" gutterBottom>
Profile
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 3 }}>
<Avatar
src={formData.avatar}
alt={formData.name || formData.username}
sx={{ width: 64, height: 64 }}
/>
<TextField
label="Avatar URL"
name="avatar"
fullWidth
value={formData.avatar}
onChange={handleChange}
/>
</Box>
<TextField
fullWidth
label="Username"
name="username"
margin="normal"
value={formData.username}
onChange={handleChange}
/>
<TextField
fullWidth
label="Full Name"
name="name"
margin="normal"
value={formData.name}
onChange={handleChange}
/>
<TextField
fullWidth
label="Email"
name="email"
type="email"
margin="normal"
value={formData.email}
onChange={handleChange}
/>
{error && (
<Alert severity="error" sx={{ mt: 2 }}>
{error}
</Alert>
)}
{success && (
<Alert severity="success" sx={{ mt: 2 }}>
{success}
</Alert>
)}
<Button
fullWidth
variant="contained"
color="primary"
sx={{ mt: 3 }}
disabled={saving || loading}
onClick={handleSave}
>
{saving ? <CircularProgress size={24} color="inherit" /> : 'Save Changes'}
</Button>
</Box>
);
}

View File

@@ -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<AuthorModel>(`/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 */ /** 🔹 Auto-load current user if token exists */
const fetchCurrentUser = async () => { const fetchCurrentUser = async () => {
if (!token) return; if (!token) return;
try { try {
const res = await api.get<AuthorModel>('/auth/me'); const me = await api.get<{ _id: string; username: string; email: string }>('/auth/me');
setCurrentUser(res.data);
} catch (err: any) { const author = await api.get<AuthorModel>(`/authors/${me.data._id}`);
const fullUser = { ...me.data, ...author.data };
setCurrentUser(fullUser);
} catch (err) {
console.error('Failed to fetch current user:', err); console.error('Failed to fetch current user:', err);
logout(); // invalid/expired token logout(); // invalid/expired token
} }
@@ -103,6 +130,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
logout, logout,
register, register,
refreshAuthors, refreshAuthors,
updateProfile,
}} }}
> >
{children} {children}

View File

@@ -17,4 +17,5 @@ export interface AuthContextModel {
register: (username: string, password: string) => Promise<void>; register: (username: string, password: string) => Promise<void>;
logout: () => void; logout: () => void;
refreshAuthors: () => Promise<void>; refreshAuthors: () => Promise<void>;
updateProfile: (user: AuthorModel) => Promise<AuthorModel | void>;
} }