From f90435399d19d502c7a3675c0eb157d753d72091 Mon Sep 17 00:00:00 2001 From: MattLeo Date: Mon, 8 Dec 2025 15:35:34 -0600 Subject: [PATCH] phase 2 UX pass --- .../src/components/ArticleEdit.jsx | 54 +++++++-- .../kb-frontend/src/components/FilterBar.jsx | 16 ++- client/kb-frontend/src/components/Toast.css | 104 ++++++++++++++++++ client/kb-frontend/src/components/Toast.jsx | 46 ++++++++ .../kb-frontend/src/hooks/useClickOutside.js | 35 ++++++ client/kb-frontend/src/hooks/useToast.js | 49 +++++++++ client/kb-frontend/src/styles/variables.css | 1 + 7 files changed, 290 insertions(+), 15 deletions(-) create mode 100644 client/kb-frontend/src/components/Toast.css create mode 100644 client/kb-frontend/src/components/Toast.jsx create mode 100644 client/kb-frontend/src/hooks/useClickOutside.js create mode 100644 client/kb-frontend/src/hooks/useToast.js diff --git a/client/kb-frontend/src/components/ArticleEdit.jsx b/client/kb-frontend/src/components/ArticleEdit.jsx index a852a54..e98243a 100644 --- a/client/kb-frontend/src/components/ArticleEdit.jsx +++ b/client/kb-frontend/src/components/ArticleEdit.jsx @@ -5,6 +5,9 @@ import 'react-quill/dist/quill.snow.css'; import './ArticleEdit.css'; import MediaGallery from './MediaGallery'; import BlotFormatter from 'quill-blot-formatter'; +import { useClickOutside } from '../hooks/useClickOutside'; +import { useToastHelpers } from '../hooks/useToast'; +import Toast from './Toast'; Quill.register('modules/blotFormatter', BlotFormatter); const BlockEmbed = Quill.import('blots/block/embed'); @@ -69,6 +72,19 @@ function ArticleEditor({ article, onSave, onCancel, token }) { const [newCategoryInput, setNewCategoryInput] = useState(''); const [showTagSuggestions, setShowTagSuggestions] = useState(false); const quillRef = useRef(null); + const categoryDropdownRef = useRef(null); + const tagDropdownRef = useRef(null); + const { toast, hideToast, showSuccess, showError } = useToastHelpers(); + + useClickOutside(categoryDropdownRef, () => { + setShowCategoryDropdown(false); + setShowCreateCategory(false); + setNewCategoryInput(''); + }, showCategoryDropdown); + + useClickOutside(tagDropdownRef, () => { + setShowTagSuggestions(false); + }, showTagSuggestions); useEffect(() => { fetchCategories(); @@ -84,27 +100,22 @@ function ArticleEditor({ article, onSave, onCancel, token }) { } }, [article]); - // Add padding and scroll when gallery opens useEffect(() => { const editorElement = document.querySelector('.article-editor'); if (isGalleryOpen) { - // Add padding equal to drawer height (40vh + some extra space) editorElement.style.paddingBottom = 'calc(40vh + 2rem)'; - - // Scroll down smoothly so editor is visible above drawer + setTimeout(() => { window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }); - }, 100); // Small delay to let padding apply first + }, 100); } else { - // Remove padding when closed editorElement.style.paddingBottom = ''; } - // Cleanup on unmount return () => { if (editorElement) { editorElement.style.paddingBottom = ''; @@ -184,16 +195,26 @@ function ArticleEditor({ article, onSave, onCancel, token }) { }; const handleSaveDraft = async () => { - await saveArticle('draft'); + try { + await saveArticle('draft'); + showSuccess('Draft saved successfully!'); + } catch (err) { + showError('Failed to save draft'); + } }; const handlePublish = async () => { if (!title.trim()) { - alert('Title is required to publish'); + showError('Title is required to publish'); return; } - await saveArticle('published'); + try { + await saveArticle('published'); + showSuccess('Article published successfully!'); + } catch (err) { + showError('Failed to publish article'); + } } const handleInsertMedia = (mediaItem) => { @@ -340,7 +361,7 @@ function ArticleEditor({ article, onSave, onCancel, token }) {
-
+
+
+ ); +} + +export default Toast; \ No newline at end of file diff --git a/client/kb-frontend/src/hooks/useClickOutside.js b/client/kb-frontend/src/hooks/useClickOutside.js new file mode 100644 index 0000000..bcc20e5 --- /dev/null +++ b/client/kb-frontend/src/hooks/useClickOutside.js @@ -0,0 +1,35 @@ +import { useEffect } from 'react'; + +/** + * Custom hook to detect clicks outside of a referenced element + * @param {React.RefObject} ref - Reference to the element to monitor + * @param {Function} handler - Callback function to execute when clicking outside + * @param {boolean} enabled - Whether the hook is active (default: true) + */ +export function useClickOutside(ref, handler, enabled = true) { + useEffect(() => { + if (!enabled) return; + + const handleClickOutside = (event) => { + if (ref.current && !ref.current.contains(event.target)) { + handler(); + } + }; + + const handleEscape = (event) => { + if (event.key === 'Escape') { + handler(); + } + }; + + // Add event listeners + document.addEventListener('mousedown', handleClickOutside); + document.addEventListener('keydown', handleEscape); + + // Cleanup + return () => { + document.removeEventListener('mousedown', handleClickOutside); + document.removeEventListener('keydown', handleEscape); + }; + }, [ref, handler, enabled]); +} \ No newline at end of file diff --git a/client/kb-frontend/src/hooks/useToast.js b/client/kb-frontend/src/hooks/useToast.js new file mode 100644 index 0000000..53a9d95 --- /dev/null +++ b/client/kb-frontend/src/hooks/useToast.js @@ -0,0 +1,49 @@ +import { useState, useCallback } from 'react'; + +/** + * Hook for managing toast notifications + * Returns: [toast state, showToast function, hideToast function] + */ +export function useToast() { + const [toast, setToast] = useState(null); + + const showToast = useCallback((message, type = 'success', duration = 3000) => { + setToast({ message, type, duration }); + }, []); + + const hideToast = useCallback(() => { + setToast(null); + }, []); + + return [toast, showToast, hideToast]; +} + +// Convenience functions +export const useToastHelpers = () => { + const [toast, showToast, hideToast] = useToast(); + + const showSuccess = useCallback((message, duration) => { + showToast(message, 'success', duration); + }, [showToast]); + + const showError = useCallback((message, duration) => { + showToast(message, 'error', duration); + }, [showToast]); + + const showWarning = useCallback((message, duration) => { + showToast(message, 'warning', duration); + }, [showToast]); + + const showInfo = useCallback((message, duration) => { + showToast(message, 'info', duration); + }, [showToast]); + + return { + toast, + hideToast, + showSuccess, + showError, + showWarning, + showInfo + }; +}; \ No newline at end of file diff --git a/client/kb-frontend/src/styles/variables.css b/client/kb-frontend/src/styles/variables.css index a80ef4c..924c581 100644 --- a/client/kb-frontend/src/styles/variables.css +++ b/client/kb-frontend/src/styles/variables.css @@ -115,6 +115,7 @@ --z-modal: 1050; --z-popover: 1060; --z-tooltip: 1070; + --z-toast: 1080; /* ===== LAYOUT ===== */ --max-width-xs: 480px;