From 83af0535b13e13e4df47527885646ce0fa963569 Mon Sep 17 00:00:00 2001 From: mukimovd <41473651-mukimovd@users.noreply.replit.com> Date: Thu, 1 May 2025 03:12:48 +0000 Subject: [PATCH] Enable users to create and curate collections of literary works Adds a CreateCollection page and updates AuthorChip to handle undefined authors. 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/731c56eb-65e0-4371-be89-8dc1c601d077.jpg --- client/src/App.tsx | 2 + client/src/components/common/AuthorChip.tsx | 33 +++- .../pages/collections/CreateCollection.tsx | 171 ++++++++++++++++++ 3 files changed, 199 insertions(+), 7 deletions(-) create mode 100644 client/src/pages/collections/CreateCollection.tsx diff --git a/client/src/App.tsx b/client/src/App.tsx index 224e3f7..3e8937d 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -11,6 +11,7 @@ import Authors from "@/pages/authors/Authors"; import WorkReading from "@/pages/works/WorkReading"; import WorkCompare from "@/pages/works/WorkCompare"; import Collections from "@/pages/collections/Collections"; +import CreateCollection from "@/pages/collections/CreateCollection"; import Profile from "@/pages/user/Profile"; import Submit from "@/pages/Submit"; @@ -24,6 +25,7 @@ function Router() { + diff --git a/client/src/components/common/AuthorChip.tsx b/client/src/components/common/AuthorChip.tsx index 29eb0f8..747f84c 100644 --- a/client/src/components/common/AuthorChip.tsx +++ b/client/src/components/common/AuthorChip.tsx @@ -3,39 +3,58 @@ import { Author } from "@shared/schema"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; interface AuthorChipProps { - author: Author; + author?: Author; size?: 'sm' | 'md' | 'lg'; withLifeDates?: boolean; className?: string; } export function AuthorChip({ author, size = 'md', withLifeDates = false, className = '' }: AuthorChipProps) { - const getAvatarSize = () => { + // Helper function to get avatar size class + function getAvatarSize() { switch (size) { case 'sm': return 'w-8 h-8'; case 'lg': return 'w-12 h-12'; default: return 'w-10 h-10'; } - }; + } - const getTextSize = () => { + // Helper function to get text size class + function getTextSize() { switch (size) { case 'sm': return 'text-sm'; case 'lg': return 'text-lg'; default: return 'text-base'; } - }; + } // Get initials for avatar fallback - const getInitials = (name: string) => { + function getInitials(name: string) { return name .split(' ') .map(part => part.charAt(0)) .join('') .toUpperCase() .slice(0, 2); - }; + } + // If author is undefined, return a placeholder + if (!author) { + return ( +
+
+ ? +
+
+

+ Unknown Author +

+
+
+ ); + } + + // If author is defined, return the full component return (
diff --git a/client/src/pages/collections/CreateCollection.tsx b/client/src/pages/collections/CreateCollection.tsx new file mode 100644 index 0000000..137f248 --- /dev/null +++ b/client/src/pages/collections/CreateCollection.tsx @@ -0,0 +1,171 @@ +import { useState } from 'react'; +import { useLocation } from 'wouter'; +import { useMutation } from '@tanstack/react-query'; +import { z } from 'zod'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { queryClient, apiRequest } from '@/lib/queryClient'; +import { useToast } from '@/hooks/use-toast'; +import { PageLayout } from '@/components/layout/PageLayout'; +import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { Textarea } from '@/components/ui/textarea'; +import { Switch } from '@/components/ui/switch'; +import { Button } from '@/components/ui/button'; +import { Loader2 } from 'lucide-react'; + +// Form validation schema +const createCollectionSchema = z.object({ + name: z.string().min(3, { message: 'Collection name must be at least 3 characters' }).max(100), + description: z.string().max(500, { message: 'Description must not exceed 500 characters' }).optional(), + isPublic: z.boolean().default(true), +}); + +type FormValues = z.infer; + +export default function CreateCollection() { + const [, navigate] = useLocation(); + const { toast } = useToast(); + + // Initialize form + const form = useForm({ + resolver: zodResolver(createCollectionSchema), + defaultValues: { + name: '', + description: '', + isPublic: true, + }, + }); + + // Create collection mutation + const createMutation = useMutation({ + mutationFn: async (values: FormValues) => { + return await apiRequest<{ id: number; slug: string }>('POST', '/api/collections', { + name: values.name, + description: values.description || '', + isPublic: values.isPublic, + userId: 1, // Using mock user ID for demo purposes + }); + }, + onSuccess: (data) => { + toast({ + title: 'Collection created', + description: 'Your collection has been created successfully', + }); + queryClient.invalidateQueries({ queryKey: ['/api/collections'] }); + navigate(`/collections`); + }, + onError: (error) => { + toast({ + title: 'Error', + description: `Failed to create collection: ${error.message}`, + variant: 'destructive', + }); + }, + }); + + const onSubmit = (values: FormValues) => { + createMutation.mutate(values); + }; + + return ( + +
+
+

+ Create a Collection +

+

+ Collections help you organize literary works into themed groups that you can share with others. +

+
+ +
+
+ + ( + + Collection Name + + + + + Choose a descriptive name for your collection + + + + )} + /> + + ( + + Description + +