ArticleEditor.tsx for Editing and Creating Articles
This commit is contained in:
@@ -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>
|
||||
|
||||
139
src/blog/components/Article/ArticleEditor.tsx
Normal file
139
src/blog/components/Article/ArticleEditor.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user