profile and update view for author
This commit is contained in:
@@ -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<number | null>(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 (
|
||||
<Profile
|
||||
onBack={handleHideProfile}
|
||||
/>
|
||||
);
|
||||
case 'article':
|
||||
if (selectedArticle == null || !articles[selectedArticle]) return null;
|
||||
return <Article article={articles[selectedArticle]} onBack={handleBack} />;
|
||||
@@ -74,16 +90,29 @@ export default function Blog(props: { disableCustomTheme?: boolean }) {
|
||||
default:
|
||||
return (
|
||||
<>
|
||||
{!currentUser && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2, gap: 1 }}>
|
||||
{!currentUser ? (
|
||||
<>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={handleShowLogin}>
|
||||
onClick={handleShowLogin}
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={() => setShowProfile(true)}
|
||||
>
|
||||
Profile
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<MainContent articles={articles} onSelectArticle={handleSelectArticle} />
|
||||
<Latest
|
||||
|
||||
166
src/blog/components/Profile.tsx
Normal file
166
src/blog/components/Profile.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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 */
|
||||
const fetchCurrentUser = async () => {
|
||||
if (!token) return;
|
||||
try {
|
||||
const res = await api.get<AuthorModel>('/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<AuthorModel>(`/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}
|
||||
|
||||
@@ -17,4 +17,5 @@ export interface AuthContextModel {
|
||||
register: (username: string, password: string) => Promise<void>;
|
||||
logout: () => void;
|
||||
refreshAuthors: () => Promise<void>;
|
||||
updateProfile: (user: AuthorModel) => Promise<AuthorModel | void>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user