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 AppTheme from '../shared-theme/AppTheme';
|
||||||
import MainContent from './components/MainContent';
|
import MainContent from './components/MainContent';
|
||||||
import ArticleView from './components/Article/ArticleView';
|
import ArticleView from './components/Article/ArticleView';
|
||||||
|
import ArticleEditor from './components/Article/ArticleEditor';
|
||||||
import Latest from './components/Latest';
|
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';
|
||||||
@@ -14,7 +15,7 @@ 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' | 'profile';
|
type View = 'home' | 'login' | 'register' | 'article' | 'profile' | 'editor';
|
||||||
|
|
||||||
export default function Blog(props: { disableCustomTheme?: boolean }) {
|
export default function Blog(props: { disableCustomTheme?: boolean }) {
|
||||||
const { articles, loading, error } = useArticles();
|
const { articles, loading, error } = useArticles();
|
||||||
@@ -24,6 +25,7 @@ export default function Blog(props: { disableCustomTheme?: boolean }) {
|
|||||||
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 [showProfile, setShowProfile] = React.useState(false);
|
||||||
|
const [showEditor, setShowEditor] = React.useState(false);
|
||||||
|
|
||||||
const handleSelectArticle = (index: number) => {
|
const handleSelectArticle = (index: number) => {
|
||||||
setSelectedArticle(index);
|
setSelectedArticle(index);
|
||||||
@@ -53,6 +55,13 @@ export default function Blog(props: { disableCustomTheme?: boolean }) {
|
|||||||
const handleHideProfile = () => {
|
const handleHideProfile = () => {
|
||||||
setShowProfile(false);
|
setShowProfile(false);
|
||||||
};
|
};
|
||||||
|
const handleShowEditor = () => {
|
||||||
|
console.log("handleShowEditor")
|
||||||
|
setShowEditor(true);
|
||||||
|
};
|
||||||
|
const handleHideEditor = () => {
|
||||||
|
setShowEditor(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(() => {
|
||||||
@@ -60,8 +69,9 @@ export default function Blog(props: { disableCustomTheme?: boolean }) {
|
|||||||
if (showRegister) return 'register';
|
if (showRegister) return 'register';
|
||||||
if (showLogin) return 'login';
|
if (showLogin) return 'login';
|
||||||
if (showProfile) return 'profile';
|
if (showProfile) return 'profile';
|
||||||
|
if (showEditor) return 'editor';
|
||||||
return 'home';
|
return 'home';
|
||||||
}, [selectedArticle, showLogin, showRegister, showProfile]);
|
}, [selectedArticle, showLogin, showRegister, showProfile, showEditor]);
|
||||||
|
|
||||||
// render function keeps JSX tidy
|
// render function keeps JSX tidy
|
||||||
const renderView = () => {
|
const renderView = () => {
|
||||||
@@ -86,6 +96,8 @@ export default function Blog(props: { disableCustomTheme?: boolean }) {
|
|||||||
case 'article':
|
case 'article':
|
||||||
if (selectedArticle == null || !articles[selectedArticle]) return null;
|
if (selectedArticle == null || !articles[selectedArticle]) return null;
|
||||||
return <ArticleView article={articles[selectedArticle]} onBack={handleBack} />;
|
return <ArticleView article={articles[selectedArticle]} onBack={handleBack} />;
|
||||||
|
case 'editor':
|
||||||
|
return <ArticleEditor author={currentUser} onBack={handleBack} ></ArticleEditor>
|
||||||
case 'home':
|
case 'home':
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
@@ -110,6 +122,13 @@ export default function Blog(props: { disableCustomTheme?: boolean }) {
|
|||||||
>
|
>
|
||||||
{currentUser.username}
|
{currentUser.username}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={handleShowEditor}
|
||||||
|
>
|
||||||
|
New Article
|
||||||
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</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 {styled} from "@mui/material/styles";
|
||||||
import Card from "@mui/material/Card";
|
import Card from "@mui/material/Card";
|
||||||
|
|
||||||
@@ -13,6 +13,12 @@ export interface ArticleProps {
|
|||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ArticleEditorProps {
|
||||||
|
article: ArticleModel;
|
||||||
|
onBack: () => void;
|
||||||
|
author: AuthorModel;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ArticleMetaProps {
|
export interface ArticleMetaProps {
|
||||||
article: ArticleModel;
|
article: ArticleModel;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user