mirror of
https://github.com/SamyRai/tercul-frontend.git
synced 2025-12-27 04:51:34 +00:00
Improve blog creation and editing experience for content creators
Removes ComponentTest, refactors BlogCreate to use BlogEditor, and fixes tag selection in TagManager. Replit-Commit-Author: Agent Replit-Commit-Session-Id: bddfbb2b-6d6b-457b-b18c-05792cdaa035 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/39b5c689-6e8a-4d5a-9792-69cc81a56534/0bdb145c-ca28-40e0-9c67-49f51753727e.jpg
This commit is contained in:
parent
ddb691f371
commit
c25c789d01
@ -21,9 +21,6 @@ import { BlogList, BlogDetail, BlogCreate } from "@/pages/blog";
|
|||||||
import Dashboard from "@/pages/dashboard/Dashboard";
|
import Dashboard from "@/pages/dashboard/Dashboard";
|
||||||
import BlogManagement from "@/pages/dashboard/BlogManagement";
|
import BlogManagement from "@/pages/dashboard/BlogManagement";
|
||||||
|
|
||||||
// Test pages
|
|
||||||
import ComponentTest from "@/pages/tests/ComponentTest";
|
|
||||||
|
|
||||||
function Router() {
|
function Router() {
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
@ -46,9 +43,6 @@ function Router() {
|
|||||||
<Route path="/dashboard" component={Dashboard} />
|
<Route path="/dashboard" component={Dashboard} />
|
||||||
<Route path="/dashboard/blog" component={BlogManagement} />
|
<Route path="/dashboard/blog" component={BlogManagement} />
|
||||||
|
|
||||||
{/* Test Routes */}
|
|
||||||
<Route path="/tests/components" component={ComponentTest} />
|
|
||||||
|
|
||||||
<Route component={NotFound} />
|
<Route component={NotFound} />
|
||||||
</Switch>
|
</Switch>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -29,7 +29,7 @@ export function TagManager({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const formTags = form.getValues(name) || [];
|
const formTags = form.getValues(name) || [];
|
||||||
if (tags && formTags.length > 0) {
|
if (tags && formTags.length > 0) {
|
||||||
const initialTags = formTags.map(id => tags.find(t => t.id === id)).filter(Boolean) as Tag[];
|
const initialTags = formTags.map((id: number) => tags.find(t => t.id === id)).filter(Boolean) as Tag[];
|
||||||
setSelectedTags(initialTags);
|
setSelectedTags(initialTags);
|
||||||
}
|
}
|
||||||
}, [tags, form, name]);
|
}, [tags, form, name]);
|
||||||
@ -58,7 +58,7 @@ export function TagManager({
|
|||||||
|
|
||||||
// Update form values
|
// Update form values
|
||||||
const currentTags = form.getValues(name) || [];
|
const currentTags = form.getValues(name) || [];
|
||||||
form.setValue(name, currentTags.filter(id => id !== tagId), { shouldValidate: true });
|
form.setValue(name, currentTags.filter((id: number) => id !== tagId), { shouldValidate: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -3,12 +3,9 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Link, useLocation } from "wouter";
|
import { Link, useLocation } from "wouter";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { ChevronLeft, Loader2 } from "lucide-react";
|
||||||
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select";
|
|
||||||
import { ChevronLeft, Plus, X, Loader2 } from "lucide-react";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
@ -18,6 +15,8 @@ import { useState } from "react";
|
|||||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { insertBlogPostSchema, Tag } from "@shared/schema";
|
import { insertBlogPostSchema, Tag } from "@shared/schema";
|
||||||
|
import { BlogEditor } from "@/components/blog/blog-editor";
|
||||||
|
import { TagManager } from "@/components/blog/tag-manager";
|
||||||
|
|
||||||
const blogPostSchema = insertBlogPostSchema.extend({
|
const blogPostSchema = insertBlogPostSchema.extend({
|
||||||
tags: z.array(z.number()).optional(),
|
tags: z.array(z.number()).optional(),
|
||||||
@ -25,49 +24,9 @@ const blogPostSchema = insertBlogPostSchema.extend({
|
|||||||
|
|
||||||
type FormValues = z.infer<typeof blogPostSchema>;
|
type FormValues = z.infer<typeof blogPostSchema>;
|
||||||
|
|
||||||
// Simple rich text editor toolbar
|
|
||||||
function EditorToolbar() {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-wrap gap-1 mb-2 border-b border-sage/20 dark:border-sage/10 pb-2">
|
|
||||||
<Button type="button" variant="ghost" size="sm" className="px-2 font-bold">
|
|
||||||
B
|
|
||||||
</Button>
|
|
||||||
<Button type="button" variant="ghost" size="sm" className="px-2 italic">
|
|
||||||
I
|
|
||||||
</Button>
|
|
||||||
<Button type="button" variant="ghost" size="sm" className="px-2 underline">
|
|
||||||
U
|
|
||||||
</Button>
|
|
||||||
<Button type="button" variant="ghost" size="sm" className="px-2">
|
|
||||||
H1
|
|
||||||
</Button>
|
|
||||||
<Button type="button" variant="ghost" size="sm" className="px-2">
|
|
||||||
H2
|
|
||||||
</Button>
|
|
||||||
<Button type="button" variant="ghost" size="sm" className="px-3">
|
|
||||||
• List
|
|
||||||
</Button>
|
|
||||||
<Button type="button" variant="ghost" size="sm" className="px-3">
|
|
||||||
1. List
|
|
||||||
</Button>
|
|
||||||
<Button type="button" variant="ghost" size="sm" className="px-2">
|
|
||||||
Link
|
|
||||||
</Button>
|
|
||||||
<Button type="button" variant="ghost" size="sm" className="px-2">
|
|
||||||
Quote
|
|
||||||
</Button>
|
|
||||||
<Button type="button" variant="ghost" size="sm" className="px-2">
|
|
||||||
Code
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function BlogCreate() {
|
export default function BlogCreate() {
|
||||||
const [, navigate] = useLocation();
|
const [, navigate] = useLocation();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [selectedTags, setSelectedTags] = useState<Tag[]>([]);
|
|
||||||
const [selectedTagId, setSelectedTagId] = useState<string>("");
|
|
||||||
const [publishNow, setPublishNow] = useState(true);
|
const [publishNow, setPublishNow] = useState(true);
|
||||||
|
|
||||||
// Fetch tags for selection
|
// Fetch tags for selection
|
||||||
@ -120,41 +79,11 @@ export default function BlogCreate() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle tag selection
|
|
||||||
const handleAddTag = () => {
|
|
||||||
if (!selectedTagId) return;
|
|
||||||
|
|
||||||
const tagId = parseInt(selectedTagId, 10);
|
|
||||||
const tag = tags?.find(t => t.id === tagId);
|
|
||||||
|
|
||||||
if (tag && !selectedTags.some(t => t.id === tag.id)) {
|
|
||||||
setSelectedTags([...selectedTags, tag]);
|
|
||||||
|
|
||||||
// Update form values
|
|
||||||
const currentTags = form.getValues("tags") || [];
|
|
||||||
form.setValue("tags", [...currentTags, tag.id]);
|
|
||||||
}
|
|
||||||
|
|
||||||
setSelectedTagId("");
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle tag removal
|
|
||||||
const handleRemoveTag = (tagId: number) => {
|
|
||||||
setSelectedTags(selectedTags.filter(tag => tag.id !== tagId));
|
|
||||||
|
|
||||||
// Update form values
|
|
||||||
const currentTags = form.getValues("tags") || [];
|
|
||||||
form.setValue("tags", currentTags.filter(id => id !== tagId));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle form submission
|
// Handle form submission
|
||||||
const onSubmit = (values: FormValues) => {
|
const onSubmit = (values: FormValues) => {
|
||||||
createMutation.mutate(values);
|
createMutation.mutate(values);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Preview content (would be processed markdown/html in a real app)
|
|
||||||
const previewContent = form.watch("content");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout>
|
<PageLayout>
|
||||||
<div className="max-w-5xl mx-auto px-4 py-8">
|
<div className="max-w-5xl mx-auto px-4 py-8">
|
||||||
@ -220,111 +149,16 @@ export default function BlogCreate() {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div>
|
<TagManager
|
||||||
<FormLabel>Tags</FormLabel>
|
name="tags"
|
||||||
<div className="flex flex-wrap gap-2 mb-2">
|
tags={tags}
|
||||||
{selectedTags.map(tag => (
|
label="Tags"
|
||||||
<Badge
|
/>
|
||||||
key={tag.id}
|
|
||||||
variant="secondary"
|
|
||||||
className="px-2 py-1 flex items-center gap-1"
|
|
||||||
>
|
|
||||||
{tag.name}
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-4 w-4 p-0 text-navy/60 dark:text-cream/60 hover:text-navy hover:dark:text-cream hover:bg-transparent"
|
|
||||||
onClick={() => handleRemoveTag(tag.id)}
|
|
||||||
>
|
|
||||||
<X className="h-3 w-3" />
|
|
||||||
<span className="sr-only">Remove {tag.name}</span>
|
|
||||||
</Button>
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{selectedTags.length === 0 && (
|
|
||||||
<p className="text-sm text-navy/60 dark:text-cream/60">
|
|
||||||
No tags selected
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Select
|
|
||||||
value={selectedTagId}
|
|
||||||
onValueChange={setSelectedTagId}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-[200px]">
|
|
||||||
<SelectValue placeholder="Select a tag" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{tags?.map(tag => (
|
|
||||||
<SelectItem
|
|
||||||
key={tag.id}
|
|
||||||
value={tag.id.toString()}
|
|
||||||
disabled={selectedTags.some(t => t.id === tag.id)}
|
|
||||||
>
|
|
||||||
{tag.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={handleAddTag}
|
|
||||||
disabled={!selectedTagId}
|
|
||||||
className="flex items-center gap-1"
|
|
||||||
>
|
|
||||||
<Plus className="h-4 w-4" />
|
|
||||||
<span>Add Tag</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<FormField
|
<BlogEditor
|
||||||
control={form.control}
|
|
||||||
name="content"
|
name="content"
|
||||||
render={({ field }) => (
|
label="Content"
|
||||||
<FormItem>
|
placeholder="Write your article content here..."
|
||||||
<FormLabel>Content</FormLabel>
|
|
||||||
<Tabs defaultValue="write">
|
|
||||||
<TabsList className="mb-2">
|
|
||||||
<TabsTrigger value="write">Write</TabsTrigger>
|
|
||||||
<TabsTrigger value="preview">Preview</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
<TabsContent value="write" className="mt-0">
|
|
||||||
<EditorToolbar />
|
|
||||||
<FormControl>
|
|
||||||
<Textarea
|
|
||||||
placeholder="Write your article content here..."
|
|
||||||
className="resize-y min-h-96 font-serif leading-relaxed"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="preview" className="mt-0">
|
|
||||||
<div className="border border-sage/20 dark:border-sage/10 rounded-md p-4 min-h-96 prose prose-russet dark:prose-invert max-w-none">
|
|
||||||
{previewContent ? (
|
|
||||||
<div dangerouslySetInnerHTML={{ __html: previewContent.replace(/\n/g, '<br/>') }} />
|
|
||||||
) : (
|
|
||||||
<p className="text-navy/60 dark:text-cream/60 italic">
|
|
||||||
Nothing to preview yet. Start writing in the "Write" tab.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
<FormDescription>
|
|
||||||
You can use markdown formatting. The article will be attributed to your account.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
null
|
|
||||||
Loading…
Reference in New Issue
Block a user