diff --git a/client/src/components/Dashboard.jsx b/client/src/components/Dashboard.jsx
new file mode 100644
index 0000000..91d863e
--- /dev/null
+++ b/client/src/components/Dashboard.jsx
@@ -0,0 +1,124 @@
+// src/components/Dashboard.jsx
+import React, { useState, useEffect } from 'react';
+import { Link, useNavigate } from 'react-router-dom';
+import axios from 'axios';
+import { format } from 'date-fns';
+import { useAuth } from '../contexts/AuthContext';
+
+export default function Dashboard() {
+ const [posts, setPosts] = useState([]);
+ const [filter, setFilter] = useState('all'); // all, draft, published
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const { logout } = useAuth();
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ loadPosts();
+ }, []);
+
+ const loadPosts = async () => {
+ try {
+ const response = await axios.get(`${import.meta.env.VITE_API_URL}/api/posts`, {
+ headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
+ });
+ setPosts(response.data);
+ setError(null);
+ } catch (err) {
+ setError('Failed to load posts');
+ console.error('Error loading posts:', err);
+ if (err.response?.status === 401) {
+ logout();
+ navigate('/login');
+ }
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleLogout = () => {
+ logout();
+ navigate('/login');
+ };
+
+ const filteredPosts = posts.filter(post => {
+ if (filter === 'all') return true;
+ return post.status === filter;
+ });
+
+ if (loading) return
Loading...
;
+
+ return (
+
+
+ My Blog Posts
+
+ New Post
+
+
+
+
+ {error &&
{error}
}
+
+
+
+
+
+
+
+
+ {filteredPosts.length === 0 ? (
+
+ No posts found. {filter === 'all' ? 'Create your first post!' : 'Try a different filter.'}
+
+ ) : (
+ filteredPosts.map(post => (
+
+
+
+ {post.title}
+
+
+ {post.status}
+
+
+
+
+ {post.published_at ? (
+
+ Published: {format(new Date(post.published_at), 'MMM d, yyyy')}
+
+ ) : (
+ Draft
+ )}
+
+
+ {post.tags && post.tags.length > 0 && (
+
+ {post.tags.map((tag, index) => (
+ {tag.name}
+ ))}
+
+ )}
+
+ ))
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/components/Editor.jsx b/client/src/components/Editor.jsx
new file mode 100644
index 0000000..2f51e5e
--- /dev/null
+++ b/client/src/components/Editor.jsx
@@ -0,0 +1,216 @@
+// src/components/Editor.jsx
+import React, { useState, useEffect } from 'react';
+import { useNavigate, useParams } from 'react-router-dom';
+import MDEditor from '@uiw/react-md-editor';
+import axios from 'axios';
+
+export default function Editor() {
+ const [title, setTitle] = useState('');
+ const [content, setContent] = useState('');
+ const [status, setStatus] = useState('draft');
+ const [publishDate, setPublishDate] = useState('');
+ const [tags, setTags] = useState([]);
+ const [tagInput, setTagInput] = useState('');
+ const [saving, setSaving] = useState(false);
+ const [error, setError] = useState('');
+ const navigate = useNavigate();
+ const { id } = useParams();
+
+ useEffect(() => {
+ if (id) {
+ loadPost();
+ }
+ }, [id]);
+
+ const loadPost = async () => {
+ try {
+ const response = await axios.get(`${import.meta.env.VITE_API_URL}/api/posts/${id}`, {
+ headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
+ });
+ const post = response.data;
+ setTitle(post.title);
+ setContent(post.markdown_content);
+ setStatus(post.status);
+ setPublishDate(post.published_at ? new Date(post.published_at).toISOString().slice(0, 16) : '');
+ setTags(post.tags?.map(tag => tag.name) || []);
+ } catch (err) {
+ console.error('Error loading post:', err);
+ setError('Failed to load post');
+ }
+ };
+
+ const handleSave = async (newStatus = status) => {
+ setSaving(true);
+ setError('');
+
+ const data = {
+ title,
+ content,
+ status: newStatus,
+ publishedAt: newStatus === 'published' ? (publishDate || new Date().toISOString()) : null,
+ tags
+ };
+
+ try {
+ if (id) {
+ await axios.put(`${import.meta.env.VITE_API_URL}/api/posts/${id}`, data, {
+ headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
+ });
+ } else {
+ await axios.post(`${import.meta.env.VITE_API_URL}/api/posts`, data, {
+ headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
+ });
+ }
+ navigate('/dashboard');
+ } catch (err) {
+ console.error('Error saving post:', err);
+ setError('Failed to save post');
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ const handleImageUpload = async (file) => {
+ if (!id) {
+ setError('Please save the post first to upload images');
+ return null;
+ }
+
+ const formData = new FormData();
+ formData.append('file', file);
+
+ try {
+ const response = await axios.post(
+ `${import.meta.env.VITE_API_URL}/api/posts/${id}/images`,
+ formData,
+ {
+ headers: {
+ Authorization: `Bearer ${localStorage.getItem('token')}`,
+ 'Content-Type': 'multipart/form-data'
+ }
+ }
+ );
+ return response.data.url;
+ } catch (err) {
+ console.error('Error uploading image:', err);
+ setError('Failed to upload image');
+ return null;
+ }
+ };
+
+ const handleDrop = async (event) => {
+ event.preventDefault();
+ const file = event.dataTransfer.files[0];
+ if (file && file.type.startsWith('image/')) {
+ const imageUrl = await handleImageUpload(file);
+ if (imageUrl) {
+ const imageMarkdown = ``;
+ setContent(prev => prev + '\n' + imageMarkdown);
+ }
+ }
+ };
+
+ const handleAddTag = (e) => {
+ if (e.key === 'Enter' && tagInput.trim()) {
+ if (!tags.includes(tagInput.trim())) {
+ setTags([...tags, tagInput.trim()]);
+ }
+ setTagInput('');
+ }
+ };
+
+ const handleRemoveTag = (tagToRemove) => {
+ setTags(tags.filter(tag => tag !== tagToRemove));
+ };
+
+ return (
+
+
+
setTitle(e.target.value)}
+ className="title-input"
+ />
+
+
+
+
+
+
+
+
+ {error &&
{error}
}
+
+ {status === 'published' && (
+
+
+
+ )}
+
+
+
+ {tags.map((tag, index) => (
+
+ {tag}
+
+
+ ))}
+
+
setTagInput(e.target.value)}
+ onKeyPress={handleAddTag}
+ />
+
+
+
e.preventDefault()}
+ >
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/styles/main.scss b/client/src/styles/main.scss
index 8919b99..53f09e1 100644
--- a/client/src/styles/main.scss
+++ b/client/src/styles/main.scss
@@ -89,4 +89,296 @@ body {
&:hover {
background: darken($primary-color, 10%);
}
+}
+
+
+.dashboard {
+ padding: 20px;
+ max-width: 1200px;
+ margin: 0 auto;
+}
+
+.dashboard-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 30px;
+
+ h1 {
+ margin: 0;
+ color: $primary-color;
+ }
+
+ .actions {
+ display: flex;
+ gap: 10px;
+
+ .button {
+ width: auto;
+ padding: 8px 16px;
+
+ &.logout {
+ background-color: $error-color;
+
+ &:hover {
+ background-color: darken($error-color, 10%);
+ }
+ }
+ }
+ }
+}
+
+.filters {
+ margin-bottom: 20px;
+ display: flex;
+ gap: 10px;
+
+ .filter-button {
+ padding: 8px 16px;
+ border: 1px solid $border-color;
+ background: white;
+ border-radius: 4px;
+ cursor: pointer;
+ transition: all 0.2s;
+
+ &.active {
+ background: $secondary-color;
+ color: white;
+ border-color: $secondary-color;
+ }
+
+ &:hover {
+ border-color: $secondary-color;
+ }
+ }
+}
+
+.posts-list {
+ display: grid;
+ gap: 20px;
+}
+
+.post-card {
+ background: white;
+ border-radius: 8px;
+ padding: 20px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+
+ .post-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 10px;
+
+ h2 {
+ margin: 0;
+ font-size: 1.5rem;
+
+ a {
+ color: $primary-color;
+ text-decoration: none;
+
+ &:hover {
+ color: $secondary-color;
+ }
+ }
+ }
+
+ .status {
+ padding: 4px 8px;
+ border-radius: 4px;
+ font-size: 0.875rem;
+ text-transform: capitalize;
+
+ &.draft {
+ background: #f1f1f1;
+ color: #666;
+ }
+
+ &.published {
+ background: #e3f2fd;
+ color: #1976d2;
+ }
+ }
+ }
+
+ .post-meta {
+ color: #666;
+ font-size: 0.875rem;
+ margin-bottom: 10px;
+ }
+
+ .tags {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+
+ .tag {
+ background: #f1f1f1;
+ padding: 4px 8px;
+ border-radius: 12px;
+ font-size: 0.875rem;
+ color: #666;
+ }
+ }
+}
+
+.loading {
+ text-align: center;
+ padding: 40px;
+ color: $text-color;
+}
+
+.error-message {
+ background: lighten($error-color, 35%);
+ color: $error-color;
+ padding: 10px;
+ border-radius: 4px;
+ margin-bottom: 20px;
+ text-align: center;
+}
+
+.no-posts {
+ text-align: center;
+ padding: 40px;
+ background: white;
+ border-radius: 8px;
+ color: #666;
+}
+
+
+.editor {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 20px;
+
+ .editor-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+
+ .title-input {
+ flex: 1;
+ font-size: 1.5rem;
+ padding: 10px;
+ border: 1px solid $border-color;
+ border-radius: 4px;
+ margin-right: 20px;
+
+ &:focus {
+ outline: none;
+ border-color: $secondary-color;
+ }
+ }
+
+ .actions {
+ display: flex;
+ gap: 10px;
+
+ .button {
+ width: auto;
+ padding: 8px 16px;
+
+ &.cancel {
+ background-color: #95a5a6;
+ }
+
+ &.draft {
+ background-color: $secondary-color;
+ }
+
+ &.publish {
+ background-color: #27ae60;
+ }
+
+ &:disabled {
+ opacity: 0.7;
+ cursor: not-allowed;
+ }
+ }
+ }
+ }
+
+ .publish-settings {
+ margin-bottom: 20px;
+ padding: 15px;
+ background: #f8f9fa;
+ border-radius: 4px;
+
+ label {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+
+ input {
+ padding: 8px;
+ border: 1px solid $border-color;
+ border-radius: 4px;
+ }
+ }
+ }
+
+ .tags-input {
+ margin-bottom: 20px;
+
+ .tags-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin-bottom: 10px;
+ }
+
+ .tag {
+ display: inline-flex;
+ align-items: center;
+ background: $secondary-color;
+ color: white;
+ padding: 4px 8px;
+ border-radius: 16px;
+ font-size: 0.875rem;
+
+ .remove-tag {
+ background: none;
+ border: none;
+ color: white;
+ margin-left: 6px;
+ cursor: pointer;
+ padding: 0 4px;
+ font-size: 1.2rem;
+ line-height: 1;
+
+ &:hover {
+ opacity: 0.8;
+ }
+ }
+ }
+
+ input {
+ width: 100%;
+ padding: 8px;
+ border: 1px solid $border-color;
+ border-radius: 4px;
+
+ &:focus {
+ outline: none;
+ border-color: $secondary-color;
+ }
+ }
+ }
+
+ .editor-container {
+ border: 1px solid $border-color;
+ border-radius: 4px;
+ overflow: hidden;
+
+ .w-md-editor {
+ border: none;
+ }
+ }
+}
+
+// Override some MD Editor default styles
+.w-md-editor {
+ --md-editor-box-shadow: none !important;
}
\ No newline at end of file