ArticleEditor.tsx for Editing and Creating Articles

This commit is contained in:
2025-11-15 03:56:47 +05:30
parent d6c84abdf6
commit 7a28dde7d5
3 changed files with 167 additions and 3 deletions

View File

@@ -6,6 +6,7 @@ import Button from '@mui/material/Button';
import AppTheme from '../shared-theme/AppTheme';
import MainContent from './components/MainContent';
import ArticleView from './components/Article/ArticleView';
import ArticleEditor from './components/Article/ArticleEditor';
import Latest from './components/Latest';
import Footer from './components/Footer';
import Login from './components/Login';
@@ -14,7 +15,7 @@ import Profile from './components/Profile';
import { useArticles } from './providers/Article';
import { useAuth } from './providers/Author';
type View = 'home' | 'login' | 'register' | 'article' | 'profile';
type View = 'home' | 'login' | 'register' | 'article' | 'profile' | 'editor';
export default function Blog(props: { disableCustomTheme?: boolean }) {
const { articles, loading, error } = useArticles();
@@ -24,6 +25,7 @@ export default function Blog(props: { disableCustomTheme?: boolean }) {
const [showLogin, setShowLogin] = React.useState(false);
const [showRegister, setShowRegister] = React.useState(false);
const [showProfile, setShowProfile] = React.useState(false);
const [showEditor, setShowEditor] = React.useState(false);
const handleSelectArticle = (index: number) => {
setSelectedArticle(index);
@@ -53,6 +55,13 @@ export default function Blog(props: { disableCustomTheme?: boolean }) {
const handleHideProfile = () => {
setShowProfile(false);
};
const handleShowEditor = () => {
console.log("handleShowEditor")
setShowEditor(true);
};
const handleHideEditor = () => {
setShowEditor(false);
};
// derive a single source of truth for view
const view: View = React.useMemo(() => {
@@ -60,8 +69,9 @@ export default function Blog(props: { disableCustomTheme?: boolean }) {
if (showRegister) return 'register';
if (showLogin) return 'login';
if (showProfile) return 'profile';
if (showEditor) return 'editor';
return 'home';
}, [selectedArticle, showLogin, showRegister, showProfile]);
}, [selectedArticle, showLogin, showRegister, showProfile, showEditor]);
// render function keeps JSX tidy
const renderView = () => {
@@ -86,6 +96,8 @@ export default function Blog(props: { disableCustomTheme?: boolean }) {
case 'article':
if (selectedArticle == null || !articles[selectedArticle]) return null;
return <ArticleView article={articles[selectedArticle]} onBack={handleBack} />;
case 'editor':
return <ArticleEditor author={currentUser} onBack={handleBack} ></ArticleEditor>
case 'home':
default:
return (
@@ -110,6 +122,13 @@ export default function Blog(props: { disableCustomTheme?: boolean }) {
>
{currentUser.username}
</Button>
<Button
variant="contained"
color="primary"
onClick={handleShowEditor}
>
New Article
</Button>
</>
)}
</Box>

View File

@@ -0,0 +1,139 @@
import * as React from 'react';
import { Box, Typography, Divider, IconButton, Chip, TextField, Button } from '@mui/material';
import { styled } from '@mui/material/styles';
import ArrowBackRoundedIcon from '@mui/icons-material/ArrowBackRounded';
import { ArticleMeta } from "../ArticleMeta";
import { ArticleEditorProps } from '../../types/props';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
const ArticleContainer = styled(Box)(({ theme }) => ({
maxWidth: '800px',
margin: '0 auto',
padding: theme.spacing(4),
[theme.breakpoints.down('sm')]: {
padding: theme.spacing(2),
},
}));
const CoverImage = styled('img')({
width: '100%',
height: 'auto',
borderRadius: '12px',
marginTop: '16px',
marginBottom: '24px',
});
export default function ArticleView({
article,
onBack,
author,
}: ArticleEditorProps) {
const [title, setTitle] = React.useState(article?.title ?? "");
const [description, setDescription] = React.useState(article?.description ?? "");
const [tag, setTag] = React.useState(article?.tag ?? "");
const [img, setImg] = React.useState(article?.img ?? "");
const [content, setContent] = React.useState(article?.content ?? "");
return (
<ArticleContainer>
{/* BACK BUTTON */}
<IconButton onClick={onBack} sx={{ mb: 2 }}>
<ArrowBackRoundedIcon />
</IconButton>
{/* TAG */}
<TextField
label="Tag"
fullWidth
value={tag}
onChange={(e) => setTag(e.target.value)}
sx={{ mb: 2 }}
/>
{/* TITLE */}
<TextField
label="Title"
fullWidth
value={title}
onChange={(e) => setTitle(e.target.value)}
sx={{ mb: 3 }}
/>
<Divider sx={{ mb: 3 }} />
{/* COVER IMAGE URL */}
<TextField
label="Cover Image URL"
fullWidth
value={img}
onChange={(e) => setImg(e.target.value)}
sx={{ mb: 2 }}
/>
{/* COVER IMAGE PREVIEW */}
{img && <CoverImage src={img} alt="cover" />}
{/* MARKDOWN EDITOR */}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<Typography variant="h6">Content</Typography>
<TextField
multiline
minRows={12}
value={content}
onChange={(e) => setContent(e.target.value)}
sx={{
'& textarea': {
fontFamily: 'monospace',
lineHeight: 1.6,
},
}}
/>
{/* LIVE PREVIEW */}
<Typography variant="h6" sx={{ mt: 4 }}>
Preview
</Typography>
<Box
sx={{
p: 2,
border: '1px solid',
borderColor: 'divider',
borderRadius: 2,
'& h3': { fontWeight: 600, mt: 4 },
'& p': { color: 'text.primary', lineHeight: 1.8, mt: 2 },
'& em': { fontStyle: 'italic' },
'& ul': { pl: 3 },
}}
>
<ReactMarkdown remarkPlugins={[remarkGfm]}>{content}</ReactMarkdown>
</Box>
</Box>
{/* ACTIONS */}
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 2, mt: 4 }}>
<Button variant="outlined" color="secondary" onClick={onBack}>
Cancel
</Button>
<Button
variant="contained"
color="primary"
onClick={() =>
console.log({
...article,
title,
tag,
img,
content,
})
}
>
Save Changes
</Button>
</Box>
</ArticleContainer>
);
}

View File

@@ -1,4 +1,4 @@
import { ArticleModel } from "./models";
import {ArticleModel, AuthorModel} from "./models";
import {styled} from "@mui/material/styles";
import Card from "@mui/material/Card";
@@ -13,6 +13,12 @@ export interface ArticleProps {
onBack: () => void;
}
export interface ArticleEditorProps {
article: ArticleModel;
onBack: () => void;
author: AuthorModel;
}
export interface ArticleMetaProps {
article: ArticleModel;
}