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 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
|
||||||
|
|||||||
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 */
|
/** 🔹 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}
|
||||||
|
|||||||
@@ -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>;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user