diff --git a/.gitignore b/.gitignore
index 82928df..2fdfa84 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,11 +1,12 @@
/server/node_modules/
/server/ka.db
+/server/media
+/server/temp_uploads
+
/client/node_modules/
-
keys.txt
-
/fd-client/DS_Store
/fd-client/fdk/
/fd-client/coverage/
diff --git a/client/kb-frontend/src/App.jsx b/client/kb-frontend/src/App.jsx
index 6489c29..d9d3d80 100644
--- a/client/kb-frontend/src/App.jsx
+++ b/client/kb-frontend/src/App.jsx
@@ -8,6 +8,7 @@ import ArticleEditor from './components/ArticleEdit';
import Login from './components/Login';
import Registration from './components/Registration'
import AdminPanel from './components/AdminPanel';
+import DraftsList from './components/DraftsList';
function App() {
const [isLoggedIn, setIsLoggedIn] = useState(false);
@@ -123,9 +124,9 @@ function App() {
if (isNew) {
fetch('http://localhost:9000/api/articles', {
method: 'POST',
- headers: {
+ headers: {
'Content-Type': 'application/json',
- 'Authorization': `Bearer ${token}`
+ 'Authorization': `Bearer ${token}`
},
body: JSON.stringify(updatedArticle)
})
@@ -139,9 +140,9 @@ function App() {
} else {
fetch(`http://localhost:9000/api/articles/${updatedArticle.ka_number}`, {
method: 'PUT',
- headers: {
+ headers: {
'Content-Type': 'application/json',
- 'Authorization': `Bearer ${token}`
+ 'Authorization': `Bearer ${token}`
},
body: JSON.stringify(updatedArticle)
})
@@ -165,20 +166,40 @@ function App() {
}
};
- const handleCreateNew = () => {
- setSelectedArticle({
- ka_number: 'New Article',
- title: '',
- content: ''
- });
- setCurrentView('edit');
+ const handleCreateNew = async () => {
+ try {
+ const response = await fetch('http://localhost:9000/api/articles', {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ title: '',
+ content: '',
+ status: 'draft'
+ })
+ });
+
+ if (response.ok) {
+ const draft = await response.json();
+ setSelectedArticle(draft);
+ setCurrentView('edit');
+ } else {
+ console.error('Failed to create draft');
+ alert('Failed to create draft');
+ }
+ } catch (error) {
+ console.error('Error creating draft:', error);
+ alert('Failed to create draft');
+ }
};
const handleDelete = () => {
if (confirm('Are you sure you want to delete this article?')) {
- fetch(`http://localhost:9000/api/articles/${selectedArticle.ka_number}`, {
+ fetch(`http://localhost:9000/api/articles/${selectedArticle.ka_number}`, {
method: 'DELETE',
- headers: {'Authorization': `Bearer ${token}`}
+ headers: { 'Authorization': `Bearer ${token}` }
})
.then(response => {
if (response.status === 401 || response.status === 403) {
@@ -211,18 +232,32 @@ function App() {
setCurrentView('list');
};
+ const handleOpenMyDrafts = () => {
+ setCurrentView('drafts');
+ };
+
+ const handleBackFromDrafts = () => {
+ setCurrentView('list');
+ };
+
+ const handleSelectDraft = (draft) => {
+ setSelectedArticle(draft);
+ setCurrentView('edit');
+ };
+
return (
{/* Show Login/Registration if not logged in */}
{!isLoggedIn ? (
authView === 'login' ? (
-
@@ -245,17 +280,23 @@ function App() {
>
) : currentView === 'detail' ? (
-
- ) : currentView === 'edit' ? (
-
+ ) : currentView === 'edit' ? (
+
) : currentView === 'admin' ? (
+ ) : currentView === 'drafts' ? (
+
) : null}
>
)}
diff --git a/client/kb-frontend/src/components/ArticleEdit.jsx b/client/kb-frontend/src/components/ArticleEdit.jsx
index e5d76d5..e7994a3 100644
--- a/client/kb-frontend/src/components/ArticleEdit.jsx
+++ b/client/kb-frontend/src/components/ArticleEdit.jsx
@@ -1,23 +1,68 @@
-import { useState } from 'react';
+import { useState, useRef } from 'react';
import ReactQuill from 'react-quill';
import 'react-quill/dist/quill.snow.css';
+import './ArticleEdit.css';
+import MediaGallery from './MediaGallery';
-function ArticleEditor ({ article, onSave, onCancel }) {
+function ArticleEditor ({ article, onSave, onCancel, token }) {
const [title, setTitle] = useState(article.title);
const [content, setContent] = useState(article.content);
+ const [isGalleryOpen, setIsGalleryOpen] = useState(false);
+ const quillRef = useRef(null);
- const handleSave = () => {
+ const handleSaveDraft = () => {
onSave({
ka_number: article.ka_number,
title: title,
- content: content
+ content: content,
+ status: 'draft'
});
};
+ const handlePublish = () => {
+ if (!title.trim()) {
+ alert('Title is required to publish');
+ return;
+ }
+
+ onSave({
+ ka_number: article.ka_number,
+ title: title,
+ content: content,
+ status: 'published'
+ });
+ }
+
+ const handleInsertMedia = (mediaItem) => {
+ const quill = quillRef.current?.getEditor();
+ if (!quill) return;
+
+ const range = quill.getSelection(true);
+ const mediaUrl = `http://localhost:9000${mediaItem.url}`;
+
+ if (mediaItem.type === 'image') {
+ quill.insertEmbed(range.index, 'image', mediaUrl);
+ } else {
+ const videoHtml = `
`;
+ quill.clipboard.dangerouslyPasteHTML(range.index, videoHtml);
+ }
+
+ quill.setSelection(range.index + 1);
+ };
+
+ const toggleGallery = () => {
+ setIsGalleryOpen(!isGalleryOpen);
+ };
+
return (
-
Editing: {article.ka_number}
+ { article.status === 'draft' ? 'Editing Draft: ' : 'Editing: '}
+ {article.ka_number}
+
+ {article.status === 'draft' && (
+ DRAFT
+ )}
@@ -34,13 +79,23 @@ function ArticleEditor ({ article, onSave, onCancel }) {
-
-
+
+
+
+
+
);
}
diff --git a/client/kb-frontend/src/components/ArticleList.css b/client/kb-frontend/src/components/ArticleList.css
index f703b4b..ea4c676 100644
--- a/client/kb-frontend/src/components/ArticleList.css
+++ b/client/kb-frontend/src/components/ArticleList.css
@@ -17,4 +17,67 @@
.article-date {
color: #666;
font-size: 0.9rem;
+}
+.editor-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.draft-indicator {
+ background-color: #f39c12;
+ color: white;
+ padding: 0.5rem 1rem;
+ border-radius: 4px;
+ font-size: 0.9rem;
+ font-weight: bold;
+}
+
+.editor-buttons {
+ display: flex;
+ gap: 1rem;
+ margin-top: 1rem;
+}
+
+.editor-buttons button {
+ padding: 0.75rem 2rem;
+ font-size: 1rem;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+}
+
+.save-draft-btn {
+ background-color: #f39c12;
+ color: white;
+}
+
+.save-draft-btn:hover {
+ background-color: #e67e22;
+}
+
+.publish-btn {
+ background-color: #27ae60;
+ color: white;
+}
+
+.publish-btn:hover {
+ background-color: #229954;
+}
+
+.cancel-btn {
+ background-color: #95a5a6;
+ color: white;
+}
+
+.cancel-btn:hover {
+ background-color: #7f8c8d;
+}
+
+.ql-editor {
+ color: #000000;
+}
+
+.ql-editor.ql-blank::before {
+ color: #999999;
}
\ No newline at end of file
diff --git a/client/kb-frontend/src/components/DraftsList.css b/client/kb-frontend/src/components/DraftsList.css
new file mode 100644
index 0000000..e4870ce
--- /dev/null
+++ b/client/kb-frontend/src/components/DraftsList.css
@@ -0,0 +1,129 @@
+.drafts-list {
+ padding: 2rem;
+ max-width: 1200px;
+ margin: 0 auto;
+}
+
+.drafts-header {
+ display: flex;
+ align-items: center;
+ gap: 2rem;
+ margin-bottom: 2rem;
+ border-bottom: 2px solid #333;
+ padding-bottom: 1rem;
+}
+
+.drafts-header h1 {
+ margin: 0;
+}
+
+.no-drafts {
+ text-align: center;
+ padding: 4rem 2rem;
+ color: #666;
+}
+
+.no-drafts p {
+ margin: 0.5rem 0;
+ font-size: 1.1rem;
+}
+
+.drafts-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+ gap: 1.5rem;
+}
+
+.draft-card {
+ background: white;
+ border: 2px solid #e0e0e0;
+ border-radius: 8px;
+ padding: 1.5rem;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+
+.draft-card:hover {
+ border-color: #3498db;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+ transform: translateY(-2px);
+}
+
+.draft-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 1rem;
+}
+
+.draft-badge {
+ background-color: #f39c12;
+ color: white;
+ padding: 0.25rem 0.75rem;
+ border-radius: 4px;
+ font-size: 0.75rem;
+ font-weight: bold;
+}
+
+.ka-number {
+ background-color: #3498db;
+ color: white;
+ padding: 0.25rem 0.5rem;
+ border-radius: 4px;
+ font-size: 0.85rem;
+}
+
+.draft-title {
+ margin: 0 0 1rem 0;
+ font-size: 1.25rem;
+ color: #2c3e50;
+}
+
+.draft-title:empty::before {
+ content: '(Untitled)';
+ color: #999;
+ font-style: italic;
+}
+
+.draft-preview {
+ color: #666;
+ font-size: 0.9rem;
+ line-height: 1.4;
+ margin-bottom: 1rem;
+ min-height: 3rem;
+}
+
+.draft-footer {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding-top: 1rem;
+ border-top: 1px solid #eee;
+}
+
+.draft-date {
+ font-size: 0.85rem;
+ color: #999;
+}
+
+.delete-draft-btn {
+ background-color: #e74c3c;
+ color: white;
+ border: none;
+ padding: 0.5rem 1rem;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 0.85rem;
+}
+
+.delete-draft-btn:hover {
+ background-color: #c0392b;
+}
+
+.error-message {
+ background-color: #ffebee;
+ color: #c62828;
+ padding: 1rem;
+ border-radius: 4px;
+ margin-bottom: 1rem;
+}
\ No newline at end of file
diff --git a/client/kb-frontend/src/components/DraftsList.jsx b/client/kb-frontend/src/components/DraftsList.jsx
new file mode 100644
index 0000000..26ea2b7
--- /dev/null
+++ b/client/kb-frontend/src/components/DraftsList.jsx
@@ -0,0 +1,110 @@
+import { useState, useEffect } from 'react';
+import './DraftsList.css';
+
+function DraftsList({ token, onBack, onSelectDraft }) {
+ const [drafts, setDrafts] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState('');
+
+ useEffect(() => {
+ fetchDrafts();
+ }, []);
+
+ const fetchDrafts = async () => {
+ try {
+ const response = await fetch('http://localhost:9000/api/articles/drafts', {
+ headers: {
+ 'Authorization': `Bearer ${token}`
+ }
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ setDrafts(data);
+ } else {
+ setError('Failed to fetch Drafts');
+ }
+ } catch(err) {
+ console.error('Error fetching drafts:', err);
+ setError('Failed to connect to server');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleDeleteDraft = async (kaNumber, e) => {
+ e.stopPropagation();
+
+ if(!confirm('Are you sure you want to delete this draft?')) return;
+
+ try {
+ const response = await fetch(`http://localhost:9000/api/articles/${kaNumber}`, {
+ method: 'DELETE',
+ headers: {
+ 'Authorization': `Bearer ${token}`
+ }
+ });
+
+ if (response.ok) {
+ setDrafts(drafts.filter(draft => draft.ka_number !== kaNumber));
+ } else {
+ alert('Failed to delete draft');
+ }
+ } catch(err) {
+ console.error('Error deleting draft:', err);
+ alert('Failed to delete draft');
+ }
+ };
+
+ if (loading) {
+ return
Loading...
;
+ }
+
+ return (
+
+
+
My Drafts
+
+
+ {error && (
{error}
)}
+
+ {drafts.length === 0 ? (
+
+
You do not have any drafts yet.
+
+ ) : (
+
+ {drafts.map(draft => (
+
onSelectDraft(draft)}
+ >
+
+ DRAFT
+ {draft.ka_number}
+
+
{draft.title || '(Untitled)'}
+
+ {draft.content
+ ? draft.content.replace(/<[^>]*>/g, '').substring(0, 100) + '...'
+ : 'No content yet'
+ }
+
+
+ Created: {new Date(draft.created_at).toLocaleDateString()}
+
+
+
+ ))}
+
+ )
+ }
+
+ );
+}
+
+export default DraftsList;
\ No newline at end of file
diff --git a/client/kb-frontend/src/components/MediaGallery.css b/client/kb-frontend/src/components/MediaGallery.css
new file mode 100644
index 0000000..c5fc014
--- /dev/null
+++ b/client/kb-frontend/src/components/MediaGallery.css
@@ -0,0 +1,242 @@
+.drawer-toggle {
+ position: fixed;
+ bottom: 0;
+ left: 50%;
+ transform: translateX(-50%);
+ background-color: #3498db;
+ color: white;
+ border: none;
+ border-radius: 8px 8px 0 0;
+ padding: 0.75rem 2rem;
+ cursor: pointer;
+ z-index: 1001;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.2);
+ transition: background-color 0.2s;
+}
+
+.drawer-toggle:hover {
+ background-color: #2980b9;
+}
+
+.drawer-toggle.open {
+ background-color: #e74c3c;
+}
+
+.drawer-toggle.open:hover {
+ background-color: #c0392b;
+}
+
+.toggle-icon {
+ font-size: 1.2rem;
+}
+
+.toggle-text {
+ font-weight: bold;
+}
+
+.media-count {
+ background-color: rgba(255, 255, 255, 0.3);
+ padding: 0.2rem 0.5rem;
+ border-radius: 12px;
+ font-size: 0.85rem;
+}
+
+.media-drawer {
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ height: 40vh;
+ background-color: white;
+ border-radius: 12px 12px 0 0;
+ box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.3);
+ z-index: 1000;
+ transform: translateY(100%);
+ transition: transform 0.3s ease-in-out;
+ display: flex;
+ flex-direction: column;
+}
+
+.media-drawer.open {
+ transform: translateY(0);
+}
+
+.drawer-handle {
+ padding: 0.75rem;
+ cursor: pointer;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ background-color: #f5f5f5;
+ border-radius: 12px 12px 0 0;
+}
+
+.drawer-handle:hover {
+ background-color: #e0e0e0;
+}
+
+.handle-bar {
+ width: 50px;
+ height: 4px;
+ background-color: #bbb;
+ border-radius: 2px;
+}
+
+.drawer-content {
+ flex: 1;
+ overflow-y: auto;
+ padding: 1rem 2rem;
+}
+
+.gallery-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 1rem;
+ padding-bottom: 1rem;
+ border-bottom: 2px solid #e0e0e0;
+}
+
+.gallery-header h3 {
+ margin: 0;
+ color: #2c3e50;
+}
+
+.upload-button {
+ background-color: #27ae60;
+ color: white;
+ padding: 0.75rem 1.5rem;
+ border-radius: 6px;
+ cursor: pointer;
+ transition: background-color 0.2s;
+ font-weight: bold;
+}
+
+.upload-button:hover {
+ background-color: #229954;
+}
+
+.loading {
+ text-align: center;
+ padding: 2rem;
+ color: #666;
+}
+
+.no-media {
+ text-align: center;
+ padding: 2rem;
+ color: #666;
+}
+
+.no-media p {
+ margin: 0.5rem 0;
+}
+
+.media-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
+ gap: 1rem;
+ padding: 1rem 0;
+}
+
+.media-item {
+ border: 2px solid #e0e0e0;
+ border-radius: 8px;
+ padding: 0.75rem;
+ background: white;
+ transition: all 0.2s;
+}
+
+.media-item:hover {
+ border-color: #3498db;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+ transform: translateY(-2px);
+}
+
+.media-item img {
+ width: 100%;
+ height: 120px;
+ object-fit: cover;
+ border-radius: 6px;
+ margin-bottom: 0.75rem;
+ cursor: pointer;
+}
+
+.video-thumbnail {
+ width: 100%;
+ height: 120px;
+ background-color: #34495e;
+ border-radius: 6px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ margin-bottom: 0.75rem;
+ color: white;
+ cursor: pointer;
+}
+
+.video-icon {
+ font-size: 2.5rem;
+}
+
+.video-label {
+ margin-top: 0.5rem;
+ font-size: 0.85rem;
+ font-weight: bold;
+}
+
+.media-actions {
+ display: flex;
+ gap: 0.5rem;
+ margin-bottom: 0.5rem;
+}
+
+.media-actions button {
+ flex: 1;
+ padding: 0.5rem;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 0.85rem;
+ font-weight: bold;
+ transition: all 0.2s;
+}
+
+.insert-btn {
+ background-color: #3498db;
+ color: white;
+}
+
+.insert-btn:hover {
+ background-color: #2980b9;
+}
+
+.delete-btn {
+ background-color: #e74c3c;
+ color: white;
+}
+
+.delete-btn:hover {
+ background-color: #c0392b;
+}
+
+.media-filename {
+ font-size: 0.75rem;
+ color: #666;
+ text-align: center;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.error-message {
+ background-color: #ffebee;
+ color: #c62828;
+ padding: 0.75rem;
+ border-radius: 4px;
+ margin-bottom: 1rem;
+}
\ No newline at end of file
diff --git a/client/kb-frontend/src/components/MediaGallery.jsx b/client/kb-frontend/src/components/MediaGallery.jsx
new file mode 100644
index 0000000..099b0bb
--- /dev/null
+++ b/client/kb-frontend/src/components/MediaGallery.jsx
@@ -0,0 +1,168 @@
+import { useState, useEffect } from 'react';
+import './MediaGallery.css';
+
+function MediaGallery({ kaNumber, token, onInsertMedia, isOpen, onToggle }) {
+ const [media, setMedia] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [uploading, setUploading] = useState(false);
+ const [error, setError] = useState('');
+
+ useEffect(() => {
+ fetchMedia();
+ }, [kaNumber, isOpen]);
+
+ const fetchMedia = async () => {
+ try {
+ const response = await fetch(`http://localhost:9000/api/articles/${kaNumber}/media`, {
+ headers: {
+ 'Authorization': `Bearer ${token}`
+ }
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ setMedia(data);
+ } else {
+ setError('Failed to fetch media');
+ }
+ } catch(err) {
+ console.error('Error fetching media:', err);
+ setError('Failed to load media');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleFileUpload = async (e) => {
+ const file = e.target.files[0];
+ if (!file) return;
+
+ setUploading(true);
+ setError('');
+
+ const formData = new FormData();
+ formData.append('file', file);
+
+ try {
+ const response = await fetch(`http://localhost:9000/api/articles/${kaNumber}/media`, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${token}`
+ },
+ body: formData
+ });
+
+ if (response.ok) {
+ const newMedia = await response.json();
+ setMedia([...media, newMedia]);
+ } else {
+ const data = await response.json();
+ setError(data.error || 'Upload failed');
+ }
+ } catch(err) {
+ console.error('Failed to uplaod file:', err);
+ setError('Failed to upload file');
+ } finally {
+ setUploading(false);
+ e.target.value = '';
+ }
+ }
+
+ const handleDelete = async (filename) => {
+ if (!confirm('Are you sure you want to delete this file?')) return;
+
+ try {
+ const response = await fetch(`http://localhost:9000/api/articles/${kaNumber}/media/${filename}`, {
+ method: 'DELETE',
+ headers: {
+ 'Authorization': `Bearer ${token}`
+ }
+ });
+
+ if (response.ok) {
+ setMedia(media.filter(m => m.filename !== filename));
+ } else {
+ alert('Failed to delete file');
+ }
+ } catch(err) {
+ console.error('Error deleting file:', err);
+ alert('Failed to delete file');
+ }
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+
+
Media Gallery - {kaNumber}
+
+
+
+ {error &&
{error}
}
+
+ {loading ? (
+
Loading media...
+ ) : media.length === 0 ? (
+
+
No media uploaded yet
+
Click "Upload Media" to add images or videos
+
+ ) : (
+
+ {media.map(item => (
+
+ {item.type === 'image' ? (
+

onInsertMedia(item)}
+ />
+ ) : (
+
onInsertMedia(item)}>
+ 🎥
+ Video
+
+ )}
+
+
+
+
+
+
{item.filename}
+
+ ))}
+
+ )}
+
+
+ >
+ );
+}
+
+export default MediaGallery;
\ No newline at end of file
diff --git a/client/kb-frontend/src/components/UserMenu.jsx b/client/kb-frontend/src/components/UserMenu.jsx
index 1655d06..196de69 100644
--- a/client/kb-frontend/src/components/UserMenu.jsx
+++ b/client/kb-frontend/src/components/UserMenu.jsx
@@ -1,7 +1,7 @@
import { useState, useEffect, useRef } from 'react';
import './UserMenu.css';
-function UserMenu({ currentUser, onLogout, onOpenAdminPanel }) {
+function UserMenu({ currentUser, onLogout, onOpenAdminPanel, onOpenMyDrafts }) {
const [isOpen, setIsOpen] = useState(false);
const menuRef = useRef(null);
@@ -25,6 +25,8 @@ function UserMenu({ currentUser, onLogout, onOpenAdminPanel }) {
action();
};
+ const canEdit = currentUser?.role === 'Admin' || currentUser?.role === 'Editor';
+
return (
)}
+ {canEdit && (
+
+ )}