Compare commits

..

10 Commits

Author SHA1 Message Date
dereklseitz
9a0ce6fb34 feat: Add KendoReact credit to login screen 2025-09-28 21:16:14 -05:00
dereklseitz
e9bd6be189 feat: add modals to Cancel and Publish buttons in Editor 2025-09-28 19:49:30 -05:00
dereklseitz
0c00c49850 maintenance: Clean up comments throughout component files 2025-09-28 16:22:05 -05:00
dereklseitz
7f576bac73 fix: Fix layout shifting with Copyright Component 2025-09-28 15:24:23 -05:00
dereklseitz
c778015244 feat: replace Vite favicon with Campfire Logs logo 2025-09-28 15:07:45 -05:00
dereklseitz
5ba65a0861 feat: add animation to LoginPage 2025-09-28 15:02:57 -05:00
dereklseitz
36670fd528 fix: resolve header image paths in production build
- Add import.meta.glob to process header images in PostCard component
- Replace manual URL construction with Vite's asset handling
- Ensures images load correctly in both dev and production environments
2025-09-28 14:11:55 -05:00
dereklseitz
3450239fe4 feat: Add demo login guidance
• Pre-populate login form with demo credentials
• Add user guidance message for demo access
2025-09-28 11:55:47 -05:00
dereklseitz
3692e08245 maintenance: Remove unused dependencies 2025-09-28 11:36:18 -05:00
dereklseitz
0dfb15466e feat: Add .env.example 2025-09-28 10:54:39 -05:00
24 changed files with 424 additions and 41 deletions

2
.env.example Normal file
View File

@@ -0,0 +1,2 @@
VITE_MOCK_USERNAME=your_username_here
VITE_MOCK_PASSWORD=your_password_here

1
.gitignore vendored
View File

@@ -1,5 +1,4 @@
.env .env
.env.*
telerik-license.txt telerik-license.txt
# Logs # Logs

View File

@@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/png" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Campfire Logs Dashboard</title> <title>Campfire Logs Dashboard</title>

View File

@@ -31,13 +31,8 @@
"@progress/kendo-react-tooltip": "^12.0.1", "@progress/kendo-react-tooltip": "^12.0.1",
"@progress/kendo-svg-icons": "^4.5.0", "@progress/kendo-svg-icons": "^4.5.0",
"@progress/kendo-theme-default": "^12.0.1", "@progress/kendo-theme-default": "^12.0.1",
"@syncfusion/ej2-react-base": "^31.1.17",
"@syncfusion/ej2-react-layouts": "^31.1.17",
"@syncfusion/ej2-react-richtexteditor": "^31.1.21",
"campfire-logs-dashboard": "file:./campfire-logs-dashboard", "campfire-logs-dashboard": "file:./campfire-logs-dashboard",
"front-matter": "^4.0.2", "front-matter": "^4.0.2",
"gray-matter": "^4.0.3",
"js-yaml": "^4.1.0",
"marked": "^16.3.0", "marked": "^16.3.0",
"react": "^19.1.1", "react": "^19.1.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",

BIN
public/derek.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 KiB

BIN
public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 516 KiB

View File

@@ -57,7 +57,7 @@ function App() {
)} )}
</IconsContext.Provider> </IconsContext.Provider>
</div> </div>
<Copyright /> <Copyright isLoginPage={location.pathname === '/login'} isLoggedIn={isLoggedIn} onLogin={handleLogin} />
</div> </div>
); );
} }

View File

@@ -46,3 +46,16 @@
.read-the-docs { .read-the-docs {
color: #888; color: #888;
} }
/* Copyright transition styles */
.copyright-login {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.copyright-normal {
position: relative;
transform: none;
}

View File

@@ -230,4 +230,53 @@ form {
width: 350px !important; width: 350px !important;
margin: 0 auto !important; margin: 0 auto !important;
contain: layout; contain: layout;
}
/* Enhanced visual styling with shadows */
/* PostCard component shadows */
.k-card {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: box-shadow 0.3s ease;
}
.k-card:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
/* Campfire theme shadow for cards */
.k-card {
box-shadow: 0 2px 8px rgba(237, 189, 125, 0.1);
}
.k-card:hover {
box-shadow: 0 4px 16px rgba(237, 189, 125, 0.3);
}
/* PanelBar component shadow */
.k-panelbar {
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
}
/* Campfire theme shadow for PanelBar */
.k-panelbar {
box-shadow: 2px 0 12px rgba(0, 0, 0, 0.3);
}
/* Button component shadows */
.k-button {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: box-shadow 0.3s ease;
}
.k-button:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
/* Campfire theme shadow for buttons */
.k-button {
box-shadow: 0 2px 8px rgba(237, 189, 125, 0.1);
}
.k-button:hover {
box-shadow: 0 4px 16px rgba(237, 189, 125, 0.3);
} }

View File

@@ -52,9 +52,35 @@ const CampfireAppBar = ({ isLoggedIn, onLogin, onDrawerToggle }) => {
<AppBarSpacer style={{ width: 20 }} /> <AppBarSpacer style={{ width: 20 }} />
{isLoggedIn ? ( {isLoggedIn ? (
<Button look="flat" onClick={handleLogout}> <>
Logout <Button look="flat" onClick={handleLogout}>
</Button> Logout
</Button>
<Avatar
rounded="full"
type="image"
style={{
marginLeft: "8px",
width: "48px !important",
height: "48px !important",
minWidth: "48px",
maxWidth: "48px",
minHeight: "48px",
maxHeight: "48px"
}}
>
<img
src="/derek.jpg"
alt="Derek Seitz"
style={{
width: "100%",
height: "100%",
border: "2px solid #edbd7d",
borderRadius: "50%"
}}
/>
</Avatar>
</>
) : ( ) : (
<Link to="/login"> <Link to="/login">
<Button look="flat">Login</Button> <Button look="flat">Login</Button>

View File

@@ -3,10 +3,12 @@ import React from 'react';
import { Button } from '@progress/kendo-react-buttons'; import { Button } from '@progress/kendo-react-buttons';
import { Card, CardImage, CardBody } from '@progress/kendo-react-layout'; import { Card, CardImage, CardBody } from '@progress/kendo-react-layout';
// Import all header images so Vite processes them
const headerImages = import.meta.glob('../../assets/header/*', { eager: true });
const PostCard = ({ post, onEdit }) => { const PostCard = ({ post, onEdit }) => {
const dataPath = post.header.image; const dataPath = post.header.image;
const relativePath = `../../${dataPath}`; const imageUrl = headerImages[`../../${dataPath}`]?.default || dataPath;
const imageUrl = new URL(relativePath, import.meta.url).href;
const formatDate = (utcString) => { const formatDate = (utcString) => {
if (!utcString) return ''; if (!utcString) return '';
@@ -25,8 +27,10 @@ const PostCard = ({ post, onEdit }) => {
const formattedDate = date.toLocaleDateString(undefined, dateOptions); const formattedDate = date.toLocaleDateString(undefined, dateOptions);
// Split formatted date to check if time component exists
const parts = formattedDate.split(','); const parts = formattedDate.split(',');
if (parts.length > 1) { if (parts.length > 1) {
// Reconstruct with separate date and time formatting for consistency
const timePart = date.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit', hour12: true }); const timePart = date.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit', hour12: true });
const datePart = date.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }); const datePart = date.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
return `${datePart}, ${timePart}`; return `${datePart}, ${timePart}`;

View File

@@ -1,6 +1,5 @@
// MarkdownEditor.jsx // MarkdownEditor.jsx
// KendoReact Splitter implementation for dual-pane markdown editing with live preview // KendoReact Splitter implementation for dual-pane markdown editing with live preview
// Demonstrates advanced layout components and real-time markdown processing
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Splitter, SplitterPane } from '@progress/kendo-react-layout'; import { Splitter, SplitterPane } from '@progress/kendo-react-layout';
import { marked } from 'marked'; import { marked } from 'marked';

View File

@@ -0,0 +1,50 @@
// CancelModal.jsx
// Confirmation modal for canceling post editing
import React from 'react';
import { Dialog, DialogActionsBar } from '@progress/kendo-react-dialogs';
import { Button } from '@progress/kendo-react-buttons';
const CancelModal = ({ isVisible, onClose, onConfirm, hasUnsavedChanges }) => {
if (!isVisible) return null;
return (
<Dialog
title="Cancel Editing"
onClose={onClose}
minWidth={400}
width={500}
style={{ borderRadius: '8px' }}
>
<div style={{ padding: '20px' }}>
{hasUnsavedChanges ? (
<>
<p style={{ margin: '0 0 16px 0', fontSize: '16px' }}>
You have unsaved changes.
</p>
<p style={{ margin: '0', fontSize: '14px', color: '#666' }}>
Are you sure you want to cancel? All unsaved changes will be lost.
</p>
</>
) : (
<p style={{ margin: '0', fontSize: '16px' }}>
Are you sure you want to cancel editing?
</p>
)}
</div>
<DialogActionsBar>
<Button onClick={onClose}>
Continue Editing
</Button>
<Button
onClick={onConfirm}
themeColor="primary"
look="outline"
>
{hasUnsavedChanges ? 'Discard Changes' : 'Cancel'}
</Button>
</DialogActionsBar>
</Dialog>
);
};
export default CancelModal;

View File

@@ -0,0 +1,51 @@
// PublishModal.jsx
// Confirmation modal for publishing posts
import React from 'react';
import { Dialog, DialogActionsBar } from '@progress/kendo-react-dialogs';
import { Button } from '@progress/kendo-react-buttons';
const PublishModal = ({ isVisible, onClose, onConfirm, postTitle }) => {
if (!isVisible) return null;
return (
<Dialog
title="Publish Post"
onClose={onClose}
minWidth={400}
width={500}
style={{ borderRadius: '8px' }}
>
<div style={{ padding: '20px' }}>
<p style={{ margin: '0 0 16px 0', fontSize: '16px' }}>
Are you sure you want to publish this post?
</p>
{postTitle && (
<p style={{
margin: '0 0 16px 0',
fontSize: '14px',
fontStyle: 'italic',
color: '#666'
}}>
"{postTitle}"
</p>
)}
<p style={{ margin: '0', fontSize: '14px', color: '#666' }}>
This will make the post visible to your readers.
</p>
</div>
<DialogActionsBar>
<Button onClick={onClose}>
Cancel
</Button>
<Button
onClick={onConfirm}
themeColor="primary"
>
Publish Post
</Button>
</DialogActionsBar>
</Dialog>
);
};
export default PublishModal;

View File

@@ -1,6 +1,5 @@
// WysiwygEditor.jsx // WysiwygEditor.jsx
// KendoReact Editor implementation for HTML editing with custom toolbar // KendoReact Editor implementation for HTML editing with custom toolbar
// Demonstrates custom tool integration and advanced editor configuration
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Editor, EditorTools, EditorToolsSettings } from '@progress/kendo-react-editor'; import { Editor, EditorTools, EditorToolsSettings } from '@progress/kendo-react-editor';
import InlineCodeTool from './custom/InlineCodeTool'; import InlineCodeTool from './custom/InlineCodeTool';

View File

@@ -1,6 +1,5 @@
// EditorModeToggle.jsx // EditorModeToggle.jsx
// Custom KendoReact component for switching between HTML and Markdown editing modes // Custom KendoReact component for switching between HTML and Markdown editing modes
// Demonstrates reusable UI components and state management patterns
import React from 'react'; import React from 'react';
import { Button } from '@progress/kendo-react-buttons'; import { Button } from '@progress/kendo-react-buttons';

View File

@@ -21,14 +21,14 @@ const InlineCodeTool = (props) => {
const hasMark = state.doc.rangeHasMark(from, to, markType); const hasMark = state.doc.rangeHasMark(from, to, markType);
if (hasMark) { if (hasMark) {
// Remove code mark if already present // Remove code mark if already present (toggle off)
tr.removeMark(from, to, markType); tr.removeMark(from, to, markType);
} else { } else {
// Add code mark to selected text // Add code mark to selected text (toggle on)
tr.addMark(from, to, markType.create()); tr.addMark(from, to, markType.create());
} }
// Dispatch transaction to apply changes // Dispatch transaction to apply changes to editor
dispatch(tr); dispatch(tr);
} }
} }

View File

@@ -7,21 +7,24 @@ import { Link } from 'react-router-dom';
import { Notification, NotificationGroup } from '@progress/kendo-react-notification'; import { Notification, NotificationGroup } from '@progress/kendo-react-notification';
const LoginComponent = ({ onLogin }) => { const LoginComponent = ({ onLogin }) => {
const [username, setUsername] = useState(''); // Get credentials from environment
const [password, setPassword] = useState(''); const mockUsername = import.meta.env.VITE_MOCK_USERNAME;
const mockPassword = import.meta.env.VITE_MOCK_PASSWORD;
// Prepopulate with demo credentials from env
const [username, setUsername] = useState(mockUsername);
const [password, setPassword] = useState(mockPassword);
const [error, setError] = useState(''); const [error, setError] = useState('');
const handleSubmit = (event) => { const handleSubmit = (event) => {
event.preventDefault(); event.preventDefault();
const mockUsername = import.meta.env.VITE_MOCK_USERNAME;
const mockPassword = import.meta.env.VITE_MOCK_PASSWORD;
if (username === mockUsername && password === mockPassword) { if (username === mockUsername && password === mockPassword) {
onLogin(true); onLogin(true);
} else { } else {
console.error("Invalid username or password"); console.error("Invalid username or password");
setError("Invalid username or password. Please try again."); setError("Invalid username or password. Please try again.");
// Clear error after 50 seconds (long timeout for demo purposes)
setTimeout(() => { setTimeout(() => {
setError(''); setError('');
}, 50000); }, 50000);
@@ -40,7 +43,15 @@ const LoginComponent = ({ onLogin }) => {
<Input style={{ border: '1px solid #edbd7d', marginBottom: '10px', width: '300px', maxWidth: '300px', minWidth: '300px' }} type="password" id="password" value={password} onChange={(e) => setPassword(e.target.value)} autoComplete="current-password" /> <Input style={{ border: '1px solid #edbd7d', marginBottom: '10px', width: '300px', maxWidth: '300px', minWidth: '300px' }} type="password" id="password" value={password} onChange={(e) => setPassword(e.target.value)} autoComplete="current-password" />
</div> </div>
<div style={{ textAlign: 'center' }}> <div style={{ textAlign: 'center', marginBottom: '10px' }}>
<p style={{
fontSize: '14px',
color: '#666',
fontStyle: 'italic',
margin: '0 0 10px 0'
}}>
Just click 'Login'. The credentials are already loaded for you.
</p>
<Button look="flat" type="submit" style={{ padding: '0 20px' }}>Login</Button> <Button look="flat" type="submit" style={{ padding: '0 20px' }}>Login</Button>
</div> </div>
<div> <div>

View File

@@ -35,7 +35,7 @@ const CampfirePanelBar = ({ isExpanded = true }) => {
const navigate = useNavigate(); const navigate = useNavigate();
const renderItem = (item) => { const renderItem = (item) => {
// External links // Handle external links (open in new tab)
if (item.url) { if (item.url) {
return ( return (
<PanelBarItem <PanelBarItem
@@ -79,7 +79,7 @@ const CampfirePanelBar = ({ isExpanded = true }) => {
); );
} }
// Internal routes // Handle internal routes (navigate within app)
if (item.route) { if (item.route) {
return ( return (
<PanelBarItem <PanelBarItem
@@ -106,7 +106,7 @@ const CampfirePanelBar = ({ isExpanded = true }) => {
); );
} }
// Headers with children // Handle section headers with child items
return ( return (
<PanelBarItem <PanelBarItem
key={item.title} key={item.title}

View File

@@ -10,10 +10,12 @@ const CampfireBreadcrumb = () => {
const path = location.pathname; const path = location.pathname;
const pathSegments = path.split('/').filter(segment => segment); const pathSegments = path.split('/').filter(segment => segment);
// Dashboard routes
if (path === '/dashboard' || path === '/') { if (path === '/dashboard' || path === '/') {
return [{ id: 'dashboard', text: 'Dashboard' }]; return [{ id: 'dashboard', text: 'Dashboard' }];
} }
// Published posts filter
if (path === '/posts') { if (path === '/posts') {
return [ return [
{ id: 'dashboard', text: 'Dashboard' }, { id: 'dashboard', text: 'Dashboard' },
@@ -21,6 +23,7 @@ const CampfireBreadcrumb = () => {
]; ];
} }
// Drafts filter
if (path === '/drafts') { if (path === '/drafts') {
return [ return [
{ id: 'dashboard', text: 'Dashboard' }, { id: 'dashboard', text: 'Dashboard' },
@@ -28,6 +31,7 @@ const CampfireBreadcrumb = () => {
]; ];
} }
// New post editor
if (path === '/editor') { if (path === '/editor') {
return [ return [
{ id: 'editor', text: 'Editor' }, { id: 'editor', text: 'Editor' },
@@ -35,6 +39,7 @@ const CampfireBreadcrumb = () => {
]; ];
} }
// Edit existing post
if (path.startsWith('/editor/')) { if (path.startsWith('/editor/')) {
const slug = pathSegments[1]; const slug = pathSegments[1];
return [ return [
@@ -43,6 +48,7 @@ const CampfireBreadcrumb = () => {
]; ];
} }
// Default fallback
return [{ id: 'dashboard', text: 'Dashboard' }]; return [{ id: 'dashboard', text: 'Dashboard' }];
}; };

View File

@@ -1,12 +1,38 @@
// Copyright.jsx // Copyright.jsx
import React from "react"; import React, { useState, useEffect } from "react";
export default function Copyright({ isLoginPage, isLoggedIn }) {
const [isCentered, setIsCentered] = useState(true);
useEffect(() => {
if (isLoginPage) {
// On login page, start centered then transition to bottom
const timer = setTimeout(() => {
setIsCentered(false);
}, 1000);
return () => clearTimeout(timer);
} else {
// Not on login page, go to bottom immediately
setIsCentered(false);
}
}, [isLoginPage]);
// Show copyright on login page with transition, or at bottom after login
// Hide completely when not on login page and not logged in
if (!isLoginPage && !isLoggedIn) {
return null;
}
export default function Copyright() {
return ( return (
<div <div
style={{ style={{
textAlign: "center", textAlign: "center",
marginTop: "1rem", marginTop: "1rem",
transition: "all 0.8s ease-in-out",
position: isLoginPage ? "fixed" : "relative",
top: isLoginPage ? (isCentered ? "65%" : "calc(90% + 3px)") : "auto",
left: isLoginPage ? "50%" : "auto",
transform: isLoginPage ? "translate(-50%, -50%)" : "none"
}} }}
> >
<p> <p>

View File

@@ -48,6 +48,7 @@ const Dashboard = React.forwardRef((props, ref) => {
}; };
const filter = getFilterFromPath(); const filter = getFilterFromPath();
// Show sections based on URL filter or show both if no filter
const showPublished = !filter || filter === 'published'; const showPublished = !filter || filter === 'published';
const showDrafts = !filter || filter === 'drafts'; const showDrafts = !filter || filter === 'drafts';

View File

@@ -1,15 +1,18 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom'; import { useParams, useNavigate } from 'react-router-dom';
import { Button } from '@progress/kendo-react-buttons'; import { Button } from '@progress/kendo-react-buttons';
import { marked } from 'marked'; import { marked } from 'marked';
import MetadataEditor from '../components/Editor/MetadataEditor'; import MetadataEditor from '../components/Editor/MetadataEditor';
import WysiwygEditor from '../components/Editor/WysiwygEditor'; import WysiwygEditor from '../components/Editor/WysiwygEditor';
import MarkdownEditor from '../components/Editor/MarkdownEditor'; import MarkdownEditor from '../components/Editor/MarkdownEditor';
import EditorModeToggle from '../components/Editor/custom/EditorModeToggle'; import EditorModeToggle from '../components/Editor/custom/EditorModeToggle';
import PublishModal from '../components/Editor/UI/PublishModal';
import CancelModal from '../components/Editor/UI/CancelModal';
import { getPostBySlug } from '../data/postsCache'; import { getPostBySlug } from '../data/postsCache';
const EditorPage = React.forwardRef((props, ref) => { const EditorPage = React.forwardRef((props, ref) => {
const { slug } = useParams(); const { slug } = useParams();
const navigate = useNavigate();
const [postData, setPostData] = useState(null); const [postData, setPostData] = useState(null);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
@@ -22,6 +25,11 @@ const EditorPage = React.forwardRef((props, ref) => {
{} {}
]); ]);
// Modal state management
const [showPublishModal, setShowPublishModal] = useState(false);
const [showCancelModal, setShowCancelModal] = useState(false);
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
useEffect(() => { useEffect(() => {
const loadPostData = async () => { const loadPostData = async () => {
setIsLoading(true); setIsLoading(true);
@@ -60,18 +68,31 @@ const EditorPage = React.forwardRef((props, ref) => {
} }
const handlePublish = () => { const handlePublish = () => {
console.log('Publish clicked', { postData }); setShowPublishModal(true);
};
const handlePublishConfirm = () => {
console.log('Publish confirmed', { postData });
setShowPublishModal(false);
// Demo functionality - would save and publish the post // Demo functionality - would save and publish the post
navigate('/dashboard');
}; };
const handleSaveDraft = () => { const handleSaveDraft = () => {
console.log('Save Draft clicked', { postData }); console.log('Save Draft clicked', { postData });
setHasUnsavedChanges(false);
// Demo functionality - would save as draft // Demo functionality - would save as draft
}; };
const handleCancel = () => { const handleCancel = () => {
console.log('Cancel clicked'); setShowCancelModal(true);
// Demo functionality - would navigate back to dashboard };
const handleCancelConfirm = () => {
console.log('Cancel confirmed');
setShowCancelModal(false);
setHasUnsavedChanges(false);
navigate('/dashboard');
}; };
// Editor mode toggle handler // Editor mode toggle handler
@@ -79,7 +100,7 @@ const EditorPage = React.forwardRef((props, ref) => {
if (editMode === 'html') { if (editMode === 'html') {
// Switch to markdown mode - keep the existing markdown content // Switch to markdown mode - keep the existing markdown content
setEditMode('markdown'); setEditMode('markdown');
// Reset splitter to 50/50 // Reset splitter to 50/50 for markdown editor
setPanes([ setPanes([
{ size: '50%' }, { size: '50%' },
{} {}
@@ -95,6 +116,13 @@ const EditorPage = React.forwardRef((props, ref) => {
// Markdown change handler // Markdown change handler
const handleMarkdownChange = (event) => { const handleMarkdownChange = (event) => {
setMarkdownContent(event.target.value); setMarkdownContent(event.target.value);
setHasUnsavedChanges(true);
};
// Content change handler for WYSIWYG editor
const handleContentChange = (newContent) => {
setContent(newContent);
setHasUnsavedChanges(true);
}; };
// Splitter change handler // Splitter change handler
@@ -119,7 +147,7 @@ const EditorPage = React.forwardRef((props, ref) => {
{editMode === 'html' ? ( {editMode === 'html' ? (
<WysiwygEditor <WysiwygEditor
content={content} content={content}
onContentChange={setContent} onContentChange={handleContentChange}
/> />
) : ( ) : (
<MarkdownEditor <MarkdownEditor
@@ -148,6 +176,21 @@ const EditorPage = React.forwardRef((props, ref) => {
Publish Publish
</Button> </Button>
</div> </div>
{/* Confirmation Modals */}
<PublishModal
isVisible={showPublishModal}
onClose={() => setShowPublishModal(false)}
onConfirm={handlePublishConfirm}
postTitle={postData?.title}
/>
<CancelModal
isVisible={showCancelModal}
onClose={() => setShowCancelModal(false)}
onConfirm={handleCancelConfirm}
hasUnsavedChanges={hasUnsavedChanges}
/>
</div> </div>
); );
}); });

View File

@@ -1,13 +1,123 @@
import React from 'react'; import React, { useState, useEffect } from 'react';
import LoginComponent from '../components/LoginComponent'; import LoginComponent from '../components/LoginComponent';
import Logo from '../assets/images/campfire_logs_square_logo_bg_match.png'; import Logo from '../assets/images/campfire_logs_square_logo_bg_match.png';
const LoginPage = React.forwardRef(({ onLogin }, ref) => { const LoginPage = React.forwardRef(({ onLogin }, ref) => {
const [showLogin, setShowLogin] = useState(false);
const [isTransitioning, setIsTransitioning] = useState(true);
const [showKendoCredit, setShowKendoCredit] = useState(false);
useEffect(() => {
// Prevent scrollbar during initial load
document.body.style.overflow = 'hidden';
// First transition: move logo and copyright to final positions
const transitionTimer = setTimeout(() => {
setIsTransitioning(false);
}, 800); // Match copyright transition timing
// Second: fade in login component after transition completes
const loginTimer = setTimeout(() => {
setShowLogin(true);
}, 1500); // Give copyright more time to transition
// Third: show KendoReact credit after login appears
const kendoTimer = setTimeout(() => {
setShowKendoCredit(true);
}, 1800); // After login form appears
// Re-enable scrolling after everything is completely done
const scrollTimer = setTimeout(() => {
document.body.style.overflow = 'auto';
}, 2000); // After all transitions complete
return () => {
clearTimeout(transitionTimer);
clearTimeout(loginTimer);
clearTimeout(kendoTimer);
clearTimeout(scrollTimer);
// Cleanup: restore scrolling if component unmounts
document.body.style.overflow = 'auto';
};
}, []);
return ( return (
<div ref={ref} style={{ textAlign: 'center', width: '400px', margin: '0 auto' }}> <div
<img src={Logo} alt="Campfire Logs Logo" width="350" height="280" style={{ display: 'block', margin: '0 auto' }} /> ref={ref}
<hr style={{ backgroundColor: '#edbd7d', height: '2px', border: 'none', width: '400px', margin: '5px auto 25px auto' }}/> style={{
<LoginComponent onLogin={onLogin} /> textAlign: 'center',
width: '400px',
margin: '0 auto',
transition: 'all 0.8s ease-in-out',
transform: isTransitioning ? 'translateY(150px)' : 'translateY(30px)',
overflow: 'hidden'
}}
>
<img
src={Logo}
alt="Campfire Logs Logo"
width="350"
height="280"
style={{
display: 'block',
margin: '0 auto',
transition: 'all 0.8s ease-in-out'
}}
/>
<hr style={{
backgroundColor: '#edbd7d',
height: '2px',
border: 'none',
width: '400px',
margin: '5px auto 25px auto'
}}/>
<div style={{
opacity: showLogin ? 1 : 0,
transition: 'opacity 0.5s ease-in-out',
visibility: showLogin ? 'visible' : 'hidden',
position: 'relative',
width: '100%',
marginTop: '20px'
}}>
<LoginComponent onLogin={onLogin} />
</div>
{/* KendoReact Credit - appears after login form */}
<div style={{
opacity: showKendoCredit ? 1 : 0,
transition: 'opacity 0.6s ease-in-out',
visibility: showKendoCredit ? 'visible' : 'hidden',
position: 'fixed',
bottom: '65px',
left: '50%',
transform: 'translateX(-50%)',
fontSize: '12px',
fontStyle: 'italic',
zIndex: 1000,
padding: '5px 0',
lineHeight: '1.2'
}}>
Powered by{' '}
<a
href="https://www.telerik.com/kendo-react-ui"
target="_blank"
rel="noopener noreferrer"
style={{
color: '#d94f27',
textDecoration: 'none',
transition: 'color 0.2s ease',
cursor: 'pointer',
display: 'inline-block',
padding: '2px 4px',
margin: '-2px -4px',
borderRadius: '2px'
}}
onMouseEnter={(e) => e.target.style.color = '#ff6f48'}
onMouseLeave={(e) => e.target.style.color = '#d94f27'}
>
KendoReact UI
</a>
</div>
</div> </div>
); );
}); });