diff --git a/src/components/CreateCellDialog.tsx b/src/components/CreateCellDialog.tsx index ca4f0c7..9ccecda 100644 --- a/src/components/CreateCellDialog.tsx +++ b/src/components/CreateCellDialog.tsx @@ -23,16 +23,24 @@ import { DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; +import { useToast } from "@/hooks/use-toast"; +import { urlLoads } from "@/lib/utils/urlLoads"; const formSchema = z.object({ title: z.string().min(3, "Title must be at least 3 characters").max(50, "Title must be less than 50 characters"), description: z.string().min(10, "Description must be at least 10 characters").max(200, "Description must be less than 200 characters"), - icon: z.string().url("Please enter a valid URL for the icon"), + icon: z + .string() + .optional() + .refine((val) => !val || val.length === 0 || URL.canParse(val), { + message: "Must be a valid URL" + }), }); export function CreateCellDialog() { const { createCell, isPostingCell } = useForum(); const { isAuthenticated } = useAuth(); + const { toast } = useToast(); const [open, setOpen] = React.useState(false); const form = useForm>({ @@ -40,12 +48,25 @@ export function CreateCellDialog() { defaultValues: { title: "", description: "", - icon: "", + icon: undefined, }, }); const onSubmit = async (values: z.infer) => { - const cell = await createCell(values.title, values.description, values.icon); + // Validate icon URL if provided + if (values.icon && values.icon.trim()) { + const ok = await urlLoads(values.icon, 5000); + if (!ok) { + toast({ + title: "Icon URL Error", + description: "Icon URL could not be loaded. Please check the URL and try again.", + variant: "destructive", + }); + return; + } + } + + const cell = await createCell(values.title, values.description, values.icon || undefined); if (cell) { setOpen(false); form.reset(); @@ -100,12 +121,13 @@ export function CreateCellDialog() { name="icon" render={({ field }) => ( - Icon URL + Icon URL (optional) diff --git a/src/components/ui/CypherImage.tsx b/src/components/ui/CypherImage.tsx index 31104b6..33f75f9 100644 --- a/src/components/ui/CypherImage.tsx +++ b/src/components/ui/CypherImage.tsx @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import { cn } from '@/lib/utils'; type CypherImageProps = { - src: string; + src?: string; alt: string; className?: string; fallbackClassName?: string; @@ -25,14 +25,14 @@ export function CypherImage({ // Generate a seed based on the alt text or src to create consistent fallbacks for the same resource const seed = generateUniqueFallback ? - (alt || src).split('').reduce((acc, char) => acc + char.charCodeAt(0), 0) : 0; + (alt || src || 'fallback').split('').reduce((acc, char) => acc + char.charCodeAt(0), 0) : 0; // Handle image load error const handleError = () => { setImageError(true); }; - if (imageError) { + if (imageError || !src || src.trim() === '') { // Generate some values based on the seed for variety const hue = (seed % 60) + 140; // Cyan-ish colors (140-200) const gridSize = (seed % 8) + 8; // 8-16px diff --git a/src/contexts/ForumContext.tsx b/src/contexts/ForumContext.tsx index 909ef7b..30e46c4 100644 --- a/src/contexts/ForumContext.tsx +++ b/src/contexts/ForumContext.tsx @@ -40,7 +40,7 @@ interface ForumContextType { createComment: (postId: string, content: string) => Promise; votePost: (postId: string, isUpvote: boolean) => Promise; voteComment: (commentId: string, isUpvote: boolean) => Promise; - createCell: (name: string, description: string, icon: string) => Promise; + createCell: (name: string, description: string, icon?: string) => Promise; refreshData: () => Promise; moderatePost: ( cellId: string, @@ -223,7 +223,7 @@ export function ForumProvider({ children }: { children: React.ReactNode }) { return result; }; - const handleCreateCell = async (name: string, description: string, icon: string): Promise => { + const handleCreateCell = async (name: string, description: string, icon?: string): Promise => { setIsPostingCell(true); const result = await createCell( name, diff --git a/src/contexts/forum/actions.ts b/src/contexts/forum/actions.ts index 65ab831..a9ce5b6 100644 --- a/src/contexts/forum/actions.ts +++ b/src/contexts/forum/actions.ts @@ -214,7 +214,7 @@ export const createComment = async ( export const createCell = async ( name: string, description: string, - icon: string, + icon: string | undefined, currentUser: User | null, isAuthenticated: boolean, toast: ToastFunction, @@ -243,7 +243,7 @@ export const createCell = async ( id: cellId, name, description, - icon, + ...(icon && { icon }), timestamp: Date.now(), author: currentUser.address }; diff --git a/src/contexts/forum/transformers.ts b/src/contexts/forum/transformers.ts index dbc20d7..9e9c0e9 100644 --- a/src/contexts/forum/transformers.ts +++ b/src/contexts/forum/transformers.ts @@ -17,7 +17,7 @@ export const transformCell = ( id: cellMessage.id, name: cellMessage.name, description: cellMessage.description, - icon: cellMessage.icon, + icon: cellMessage.icon || '', signature: cellMessage.signature, browserPubKey: cellMessage.browserPubKey }; diff --git a/src/contexts/forum/types.ts b/src/contexts/forum/types.ts index 3a9edd0..1e6651c 100644 --- a/src/contexts/forum/types.ts +++ b/src/contexts/forum/types.ts @@ -21,6 +21,6 @@ export interface ForumContextType { createComment: (postId: string, content: string) => Promise; votePost: (postId: string, isUpvote: boolean) => Promise; voteComment: (commentId: string, isUpvote: boolean) => Promise; - createCell: (name: string, description: string, icon: string) => Promise; + createCell: (name: string, description: string, icon?: string) => Promise; refreshData: () => Promise; } \ No newline at end of file diff --git a/src/lib/utils/urlLoads.ts b/src/lib/utils/urlLoads.ts new file mode 100644 index 0000000..4ff70ee --- /dev/null +++ b/src/lib/utils/urlLoads.ts @@ -0,0 +1,20 @@ +/** + * Utility to check if a URL loads successfully within a timeout + */ +export async function urlLoads(url: string, timeoutMs: number = 5000): Promise { + try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeoutMs); + + const response = await fetch(url, { + method: 'HEAD', + signal: controller.signal, + cache: 'no-cache' + }); + + clearTimeout(timeoutId); + return response.ok; + } catch (error) { + return false; + } +} \ No newline at end of file diff --git a/src/lib/waku/messages_parser.ts b/src/lib/waku/messages_parser.ts index 97c506a..368777e 100644 --- a/src/lib/waku/messages_parser.ts +++ b/src/lib/waku/messages_parser.ts @@ -11,7 +11,7 @@ export function cellToMessage(cell: Cell, sender: string): CellMessage { id: cell.id, name: cell.name, description: cell.description, - icon: cell.icon + ...(cell.icon && { icon: cell.icon }) }; } @@ -20,7 +20,7 @@ export function messageToCell(message: CellMessage): Cell { id: message.id, name: message.name, description: message.description, - icon: message.icon + icon: message.icon || '' }; }