From 64fd506bdc66c33f89ab34de4535972b4a6556c5 Mon Sep 17 00:00:00 2001 From: mukimovd <41473651-mukimovd@users.noreply.replit.com> Date: Thu, 1 May 2025 03:34:53 +0000 Subject: [PATCH] Add blog feature for sharing articles and engaging with literary content Implements BlogList, BlogDetail, and BlogCreate components with corresponding types and API integrations. Replit-Commit-Author: Agent Replit-Commit-Session-Id: cbacfb18-842a-4116-a907-18c0105ad8ec Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/39b5c689-6e8a-4d5a-9792-69cc81a56534/ef0e7cfe-4f63-478f-9d08-4bb22d0da15e.jpg --- client/src/App.tsx | 1 + client/src/lib/types.ts | 32 +++ client/src/pages/blog/BlogCreate.tsx | 364 +++++++++++++++++++++++++ client/src/pages/blog/BlogDetail.tsx | 392 +++++++++++++++++++++++++++ client/src/pages/blog/BlogList.tsx | 212 +++++++++++++++ client/src/pages/blog/index.ts | 3 + 6 files changed, 1004 insertions(+) create mode 100644 client/src/pages/blog/BlogCreate.tsx create mode 100644 client/src/pages/blog/BlogDetail.tsx create mode 100644 client/src/pages/blog/BlogList.tsx create mode 100644 client/src/pages/blog/index.ts diff --git a/client/src/App.tsx b/client/src/App.tsx index 3e8937d..7451a88 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -14,6 +14,7 @@ import Collections from "@/pages/collections/Collections"; import CreateCollection from "@/pages/collections/CreateCollection"; import Profile from "@/pages/user/Profile"; import Submit from "@/pages/Submit"; +import { BlogList, BlogDetail, BlogCreate } from "@/pages/blog"; function Router() { return ( diff --git a/client/src/lib/types.ts b/client/src/lib/types.ts index d59d305..37f919f 100644 --- a/client/src/lib/types.ts +++ b/client/src/lib/types.ts @@ -96,3 +96,35 @@ export interface Annotation { likes: number; liked: boolean; } + +export interface BlogPostWithDetails { + id: number; + title: string; + slug: string; + content: string; + authorId: number; + excerpt?: string; + publishedAt: Date | null; + createdAt: Date; + author?: Omit; + tags?: Tag[]; + commentCount?: number; + likeCount?: number; +} + +export interface BlogPostListItem { + id: number; + title: string; + slug: string; + excerpt?: string; + publishedAt: Date | null; + createdAt: Date; + author?: { + id: number; + displayName: string | null; + avatar: string | null; + }; + tags?: Tag[]; + commentCount: number; + likeCount: number; +} diff --git a/client/src/pages/blog/BlogCreate.tsx b/client/src/pages/blog/BlogCreate.tsx new file mode 100644 index 0000000..742bf90 --- /dev/null +++ b/client/src/pages/blog/BlogCreate.tsx @@ -0,0 +1,364 @@ +import { PageLayout } from "@/components/layout/PageLayout"; +import { Button } from "@/components/ui/button"; +import { Link, useLocation } from "wouter"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; +import { Switch } from "@/components/ui/switch"; +import { Label } from "@/components/ui/label"; +import { Badge } from "@/components/ui/badge"; +import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select"; +import { ChevronLeft, Plus, X, Loader2 } from "lucide-react"; +import { z } from "zod"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; +import { apiRequest, queryClient } from "@/lib/queryClient"; +import { useState } from "react"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { useToast } from "@/hooks/use-toast"; +import { insertBlogPostSchema } from "@shared/schema"; +import { Tag } from "@/lib/types"; + +const blogPostSchema = insertBlogPostSchema.extend({ + tags: z.array(z.number()).optional(), +}); + +type FormValues = z.infer; + +// Simple rich text editor toolbar +function EditorToolbar() { + return ( +
+ + + + + + + + + + +
+ ); +} + +export default function BlogCreate() { + const [, navigate] = useLocation(); + const { toast } = useToast(); + const [selectedTags, setSelectedTags] = useState([]); + const [selectedTagId, setSelectedTagId] = useState(""); + const [publishNow, setPublishNow] = useState(true); + + // Fetch tags for selection + const { data: tags } = useQuery({ + queryKey: ['/api/tags'], + }); + + // Form definition + const form = useForm({ + resolver: zodResolver(blogPostSchema), + defaultValues: { + title: "", + content: "", + excerpt: "", + authorId: 1, // Mock user ID for demo + tags: [], + }, + }); + + // Create blog post mutation + const createMutation = useMutation({ + mutationFn: async (values: FormValues) => { + // If publishNow is true, set publishedAt to current date + const postData = publishNow + ? { ...values, publishedAt: new Date().toISOString() } + : values; + + return await apiRequest('POST', '/api/blog', postData); + }, + onSuccess: () => { + toast({ + title: "Success", + description: publishNow + ? "Your article has been published" + : "Your article has been saved as a draft", + }); + + // Invalidate queries to refresh the data + queryClient.invalidateQueries({ queryKey: ['/api/blog'] }); + + // Navigate to blog list + navigate("/blog"); + }, + onError: (error) => { + toast({ + title: "Error", + description: "Failed to create blog post", + variant: "destructive", + }); + }, + }); + + // 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 + const onSubmit = (values: FormValues) => { + createMutation.mutate(values); + }; + + // Preview content (would be processed markdown/html in a real app) + const previewContent = form.watch("content"); + + return ( + +
+
+ + + + +

+ Write an Article +

+

+ Share your knowledge, insights, and perspectives on literature and translations +

+
+ +
+
+ + ( + + Title + + + + + + )} + /> + + ( + + Excerpt (Summary) + +