Created server and client

This commit is contained in:
2024-12-01 13:17:12 +00:00
parent c4e4affa7c
commit 116b4be72f
11 changed files with 918 additions and 30 deletions

47
client/src/App.js Normal file
View File

@@ -0,0 +1,47 @@
import React from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import Login from './components/Login';
import Dashboard from './components/Dashboard';
import Editor from './components/Editor';
function PrivateRoute({ children }) {
const token = localStorage.getItem('token');
return token ? children : <Navigate to="/login" />;
}
export default function App() {
return (
<Router>
<div className="container">
<Routes>
<Route path="/login" element={<Login />} />
<Route
path="/dashboard"
element={
<PrivateRoute>
<Dashboard />
</PrivateRoute>
}
/>
<Route
path="/new"
element={
<PrivateRoute>
<Editor />
</PrivateRoute>
}
/>
<Route
path="/edit/:id"
element={
<PrivateRoute>
<Editor />
</PrivateRoute>
}
/>
<Route path="/" element={<Navigate to="/dashboard" />} />
</Routes>
</div>
</Router>
);
}

View File

@@ -0,0 +1,82 @@
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import axios from 'axios';
import { format } from 'date-fns';
export default function Dashboard() {
const [posts, setPosts] = useState([]);
const [filter, setFilter] = useState('all'); // all, draft, published
useEffect(() => {
loadPosts();
}, []);
const loadPosts = async () => {
try {
const response = await axios.get('/api/posts', {
headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
});
setPosts(response.data);
} catch (error) {
console.error('Error loading posts:', error);
}
};
const filteredPosts = posts.filter(post => {
if (filter === 'all') return true;
return post.status === filter;
});
return (
<div className="dashboard">
<div className="dashboard-header">
<h1>My Posts</h1>
<Link to="/new" className="button">New Post</Link>
</div>
<div className="filters">
<button
className={`filter ${filter === 'all' ? 'active' : ''}`}
onClick={() => setFilter('all')}
>
All
</button>
<button
className={`filter ${filter === 'draft' ? 'active' : ''}`}
onClick={() => setFilter('draft')}
>
Drafts
</button>
<button
className={`filter ${filter === 'published' ? 'active' : ''}`}
onClick={() => setFilter('published')}
>
Published
</button>
</div>
<div className="posts-list">
{filteredPosts.map(post => (
<div key={post.id} className="post-item">
<div className="post-title">
<Link to={`/edit/${post.id}`}>{post.title}</Link>
<span className={`status ${post.status}`}>{post.status}</span>
</div>
<div className="post-meta">
<span className="date">
{post.published_at
? format(new Date(post.published_at), 'MMM d, yyyy')
: 'Not published'}
</span>
<div className="tags">
{post.tags.map((tag, index) => (
<span key={index} className="tag">{tag}</span>
))}
</div>
</div>
</div>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,154 @@
import React, { useState, useEffect } from 'react';
import MDEditor from '@uiw/react-md-editor';
import { useNavigate, useParams } from 'react-router-dom';
import axios from 'axios';
export default function Editor() {
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const [tags, setTags] = useState([]);
const [tagInput, setTagInput] = useState('');
const [publishDate, setPublishDate] = useState('');
const { id } = useParams();
const navigate = useNavigate();
useEffect(() => {
if (id) {
loadPost();
}
}, [id]);
const loadPost = async () => {
const response = await axios.get(`/api/posts/${id}`, {
headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
});
const { title, markdown_content, tags, published_at } = response.data;
setTitle(title);
setContent(markdown_content);
setTags(tags);
setPublishDate(published_at);
};
const handleSave = async (status) => {
const data = {
title,
content,
tags,
status,
publishedAt: publishDate || new Date().toISOString()
};
try {
if (id) {
await axios.put(`/api/posts/${id}`, data, {
headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
});
} else {
await axios.post('/api/posts', data, {
headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
});
}
navigate('/dashboard');
} catch (error) {
console.error('Error saving post:', error);
}
};
const handleImageUpload = async (file) => {
const formData = new FormData();
formData.append('image', file);
try {
const response = await axios.post(`/api/images/${id}`, formData, {
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
'Content-Type': 'multipart/form-data'
}
});
return response.data.url;
} catch (error) {
console.error('Error uploading image:', error);
return null;
}
};
return (
<div className="post-editor">
<input
type="text"
placeholder="Post title"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<input
type="datetime-local"
value={publishDate}
onChange={(e) => setPublishDate(e.target.value)}
/>
<div className="tags-input">
{tags.map((tag, index) => (
<span key={index} className="tag">
{tag}
<span
className="remove"
onClick={() => setTags(tags.filter((_, i) => i !== index))}
>
×
</span>
</span>
))}
<input
type="text"
placeholder="Add a tag"
value={tagInput}
onChange={(e) => setTagInput(e.target.value)}
onKeyPress={(e) => {
if (e.key === 'Enter' && tagInput.trim()) {
setTags([...tags, tagInput.trim()]);
setTagInput('');
}
}}
/>
</div>
<MDEditor
value={content}
onChange={setContent}
preview="edit-preview"
height={500}
commands={[
{
name: 'image',
keyCommand: 'image',
buttonProps: { 'aria-label': 'Insert image' },
execute: async (state, api) => {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.onchange = async () => {
const file = input.files[0];
const imageUrl = await handleImageUpload(file);
if (imageUrl) {
const imageMarkdown = `![${file.name}](${imageUrl})`;
api.replaceSelection(imageMarkdown);
}
};
input.click();
},
},
]}
/>
<div className="buttons">
<button className="button draft" onClick={() => handleSave('draft')}>
Save as Draft
</button>
<button className="button" onClick={() => handleSave('published')}>
Publish
</button>
</div>
</div>
);
}

View File

@@ -0,0 +1,52 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import axios from 'axios';
export default function Login() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const navigate = useNavigate();
const handleSubmit = async (e) => {
e.preventDefault();
try {
const response = await axios.post('/api/auth/login', {
username,
password
});
localStorage.setItem('token', response.data.token);
navigate('/dashboard');
} catch (error) {
setError('Invalid credentials');
}
};
return (
<div className="login-container">
<form onSubmit={handleSubmit} className="login-form">
<h2>Login</h2>
{error && <div className="error">{error}</div>}
<div className="form-group">
<label>Username</label>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
/>
</div>
<div className="form-group">
<label>Password</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<button type="submit" className="button">Login</button>
</form>
</div>
);
}

View File

@@ -0,0 +1,66 @@
$primary-color: #2c3e50;
$secondary-color: #3498db;
$background-color: #f5f6fa;
$text-color: #2c3e50;
$border-color: #dcdde1;
body {
background-color: $background-color;
color: $text-color;
font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.post-editor {
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
padding: 20px;
margin-bottom: 20px;
input[type="text"] {
width: 100%;
padding: 10px;
margin-bottom: 20px;
border: 1px solid $border-color;
border-radius: 4px;
}
.tags-input {
margin-bottom: 20px;
.tag {
display: inline-block;
background: $secondary-color;
color: white;
padding: 5px 10px;
border-radius: 15px;
margin-right: 10px;
margin-bottom: 5px;
.remove {
margin-left: 5px;
cursor: pointer;
}
}
}
.button {
background: $primary-color;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
margin-right: 10px;
&.draft {
background: $secondary-color;
}
}
}