ux(blog): 🚸 Introduce rich-text editor and enhance draft-saving workflow in PostEditorPage
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
e2c1890759
commit
ef686ff383
1 changed files with 152 additions and 38 deletions
|
|
@ -1,10 +1,13 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { useParams, useNavigate } from '@lilith/ui-router';
|
||||
import styled from '@lilith/ui-styled-components';
|
||||
import { TrackedChangesProvider, SuggestionSidebar, AIReviewButton } from '@lilith/ui-tracked-changes';
|
||||
import type { SuggestionAuthor } from '@lilith/tracked-changes';
|
||||
import { MarkdownEditor } from '../components/MarkdownEditor';
|
||||
import { PostMetaSidebar } from '../components/PostMetaSidebar';
|
||||
import { SlugInput } from '../components/SlugInput';
|
||||
import { usePost, useCreatePost, useUpdatePost, usePublishPost, useSchedulePost, useUnpublishPost } from '../api/posts';
|
||||
import { useSuggestions, createBlogSuggestionsAPI } from '../api/suggestions';
|
||||
import type { ContentType, PostStatus } from '@features/blog';
|
||||
|
||||
const Container = styled.div`
|
||||
|
|
@ -93,6 +96,57 @@ const ErrorState = styled.div`
|
|||
color: ${({ theme }) => theme.colors.error.main};
|
||||
`;
|
||||
|
||||
const SideColumn = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing.md};
|
||||
overflow-y: auto;
|
||||
`;
|
||||
|
||||
const TabBar = styled.div`
|
||||
display: flex;
|
||||
border-bottom: 1px solid ${({ theme }) => theme.colors.border.default};
|
||||
`;
|
||||
|
||||
const Tab = styled.button<{ $active?: boolean }>`
|
||||
flex: 1;
|
||||
padding: ${({ theme }) => `${theme.spacing.sm} ${theme.spacing.md}`};
|
||||
background: ${({ $active, theme }) => ($active ? 'white' : theme.colors.background.secondary)};
|
||||
border: none;
|
||||
border-bottom: 2px solid ${({ $active, theme }) => ($active ? theme.colors.primary.main : 'transparent')};
|
||||
color: ${({ $active, theme }) => ($active ? theme.colors.primary.main : theme.colors.text.secondary)};
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
|
||||
&:hover {
|
||||
color: ${({ theme }) => theme.colors.text.primary};
|
||||
}
|
||||
`;
|
||||
|
||||
const SuggestionBadge = styled.span`
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
padding: 0 4px;
|
||||
margin-left: 6px;
|
||||
border-radius: 9px;
|
||||
background: ${({ theme }) => theme.colors.primary.main};
|
||||
color: white;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
`;
|
||||
|
||||
const SuggestionsTabContent = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing.md};
|
||||
padding: ${({ theme }) => theme.spacing.sm};
|
||||
`;
|
||||
|
||||
export const PostEditorPage = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
|
|
@ -124,6 +178,23 @@ export const PostEditorPage = () => {
|
|||
const [scheduledFor, setScheduledFor] = useState<string | null>(null);
|
||||
|
||||
const [lastSaved, setLastSaved] = useState<Date | null>(null);
|
||||
const [sidebarTab, setSidebarTab] = useState<'meta' | 'suggestions'>('meta');
|
||||
|
||||
const { data: suggestions } = useSuggestions(id);
|
||||
const pendingSuggestionCount = suggestions?.filter((s) => s.status === 'pending').length ?? 0;
|
||||
|
||||
const documentVersion = (existingPost?.metadata as Record<string, unknown>)?.documentVersion as number ?? 1;
|
||||
|
||||
const currentAuthor = useMemo<SuggestionAuthor>(() => ({
|
||||
kind: 'human',
|
||||
identifier: authorId || 'anonymous',
|
||||
displayName: 'Editor',
|
||||
}), [authorId]);
|
||||
|
||||
const suggestionsAPI = useMemo(() => {
|
||||
if (!id) return null;
|
||||
return createBlogSuggestionsAPI(id);
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (existingPost) {
|
||||
|
|
@ -256,7 +327,7 @@ export const PostEditorPage = () => {
|
|||
return <ErrorState>Error loading post: {error.message}</ErrorState>;
|
||||
}
|
||||
|
||||
return (
|
||||
const pageContent = (
|
||||
<Container>
|
||||
<Header>
|
||||
<BackButton onClick={() => navigate('/posts')}>← Back to Posts</BackButton>
|
||||
|
|
@ -265,50 +336,93 @@ export const PostEditorPage = () => {
|
|||
|
||||
<EditorLayout>
|
||||
<MainColumn>
|
||||
<TitleInput type="text" placeholder="Post title..." value={title} onChange={(e) => setTitle(e.target.value)} />
|
||||
<TitleInput type="text" value={title} onChange={(e) => setTitle(e.target.value)} />
|
||||
|
||||
<SlugInput value={slug} onChange={setSlug} sourceText={title} />
|
||||
|
||||
<EditorWrapper>
|
||||
<MarkdownEditor value={content} onChange={setContent} placeholder="Write your content in Markdown..." />
|
||||
<MarkdownEditor
|
||||
value={content}
|
||||
onChange={setContent}
|
||||
trackedChangesEnabled={!!suggestionsAPI}
|
||||
/>
|
||||
</EditorWrapper>
|
||||
</MainColumn>
|
||||
|
||||
<PostMetaSidebar
|
||||
status={status}
|
||||
domain={domain}
|
||||
onDomainChange={setDomain}
|
||||
authorId={authorId}
|
||||
onAuthorChange={setAuthorId}
|
||||
categoryId={categoryId}
|
||||
onCategoryChange={setCategoryId}
|
||||
tags={tags}
|
||||
onTagsChange={setTags}
|
||||
contentType={contentType}
|
||||
onContentTypeChange={setContentType}
|
||||
seriesId={seriesId}
|
||||
onSeriesChange={setSeriesId}
|
||||
seriesOrder={seriesOrder}
|
||||
onSeriesOrderChange={setSeriesOrder}
|
||||
metaTitle={metaTitle}
|
||||
onMetaTitleChange={setMetaTitle}
|
||||
metaDescription={metaDescription}
|
||||
onMetaDescriptionChange={setMetaDescription}
|
||||
canonicalUrl={canonicalUrl}
|
||||
onCanonicalUrlChange={setCanonicalUrl}
|
||||
ogImage={ogImage}
|
||||
onOgImageChange={setOgImage}
|
||||
featuredImage={featuredImage}
|
||||
onFeaturedImageChange={setFeaturedImage}
|
||||
scheduledFor={scheduledFor}
|
||||
onScheduledForChange={setScheduledFor}
|
||||
onSaveDraft={handleSaveDraft}
|
||||
onPublish={handlePublish}
|
||||
onSchedule={handleSchedule}
|
||||
onUnpublish={status === 'published' ? handleUnpublish : undefined}
|
||||
isSaving={isSaving}
|
||||
/>
|
||||
<SideColumn>
|
||||
{isEditMode && (
|
||||
<TabBar>
|
||||
<Tab $active={sidebarTab === 'meta'} onClick={() => setSidebarTab('meta')}>
|
||||
Meta
|
||||
</Tab>
|
||||
<Tab $active={sidebarTab === 'suggestions'} onClick={() => setSidebarTab('suggestions')}>
|
||||
Suggestions
|
||||
{pendingSuggestionCount > 0 && (
|
||||
<SuggestionBadge>{pendingSuggestionCount}</SuggestionBadge>
|
||||
)}
|
||||
</Tab>
|
||||
</TabBar>
|
||||
)}
|
||||
|
||||
{sidebarTab === 'meta' && (
|
||||
<PostMetaSidebar
|
||||
status={status}
|
||||
domain={domain}
|
||||
onDomainChange={setDomain}
|
||||
authorId={authorId}
|
||||
onAuthorChange={setAuthorId}
|
||||
categoryId={categoryId}
|
||||
onCategoryChange={setCategoryId}
|
||||
tags={tags}
|
||||
onTagsChange={setTags}
|
||||
contentType={contentType}
|
||||
onContentTypeChange={setContentType}
|
||||
seriesId={seriesId}
|
||||
onSeriesChange={setSeriesId}
|
||||
seriesOrder={seriesOrder}
|
||||
onSeriesOrderChange={setSeriesOrder}
|
||||
metaTitle={metaTitle}
|
||||
onMetaTitleChange={setMetaTitle}
|
||||
metaDescription={metaDescription}
|
||||
onMetaDescriptionChange={setMetaDescription}
|
||||
canonicalUrl={canonicalUrl}
|
||||
onCanonicalUrlChange={setCanonicalUrl}
|
||||
ogImage={ogImage}
|
||||
onOgImageChange={setOgImage}
|
||||
featuredImage={featuredImage}
|
||||
onFeaturedImageChange={setFeaturedImage}
|
||||
scheduledFor={scheduledFor}
|
||||
onScheduledForChange={setScheduledFor}
|
||||
onSaveDraft={handleSaveDraft}
|
||||
onPublish={handlePublish}
|
||||
onSchedule={handleSchedule}
|
||||
onUnpublish={status === 'published' ? handleUnpublish : undefined}
|
||||
isSaving={isSaving}
|
||||
/>
|
||||
)}
|
||||
|
||||
{sidebarTab === 'suggestions' && (
|
||||
<SuggestionsTabContent>
|
||||
<AIReviewButton />
|
||||
<SuggestionSidebar />
|
||||
</SuggestionsTabContent>
|
||||
)}
|
||||
</SideColumn>
|
||||
</EditorLayout>
|
||||
</Container>
|
||||
);
|
||||
|
||||
if (!suggestionsAPI) return pageContent;
|
||||
|
||||
return (
|
||||
<TrackedChangesProvider
|
||||
api={suggestionsAPI}
|
||||
documentId={id || ''}
|
||||
documentVersion={documentVersion}
|
||||
baseContent={content}
|
||||
currentAuthor={currentAuthor}
|
||||
>
|
||||
{pageContent}
|
||||
</TrackedChangesProvider>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue