diff --git a/apps/cursor/src/components/company/add-company-button.tsx b/apps/cursor/src/components/company/add-company-button.tsx index a3979460..467650e1 100644 --- a/apps/cursor/src/components/company/add-company-button.tsx +++ b/apps/cursor/src/components/company/add-company-button.tsx @@ -1,21 +1,50 @@ "use client"; +import { usePathname, useRouter } from "next/navigation"; import { parseAsBoolean, useQueryStates } from "nuqs"; +import { useEffect, useState } from "react"; +import { createClient } from "@/utils/supabase/client"; import { Button } from "../ui/button"; export function AddCompanyButton({ redirect }: { redirect?: boolean }) { + const router = useRouter(); + const pathname = usePathname(); + const supabase = createClient(); + const [isAuthenticated, setIsAuthenticated] = useState(null); + const [_, setAddCompany] = useQueryStates({ addCompany: parseAsBoolean.withDefault(false), redirect: parseAsBoolean.withDefault(redirect ?? false), }); + useEffect(() => { + async function getUser() { + const session = await supabase.auth.getSession(); + + setIsAuthenticated(Boolean(session.data.session)); + } + + getUser(); + }, []); + + const handleClick = () => { + // Adding a company requires an authenticated session: the save action is + // auth-guarded and the logo upload hits an auth-only storage policy. Send + // signed-out visitors to sign in instead of into a form that can't succeed. + if (isAuthenticated === false) { + router.push(`/login?next=${pathname}`); + return; + } + + if (isAuthenticated === null) { + return; + } + + setAddCompany({ addCompany: true, redirect }); + }; + return ( - ); diff --git a/apps/cursor/src/components/upload-logo.tsx b/apps/cursor/src/components/upload-logo.tsx index dead3681..c1c6ec7d 100644 --- a/apps/cursor/src/components/upload-logo.tsx +++ b/apps/cursor/src/components/upload-logo.tsx @@ -3,6 +3,7 @@ import { PlusIcon } from "lucide-react"; import Image from "next/image"; import { type ChangeEvent, type DragEvent, useRef, useState } from "react"; +import { toast } from "sonner"; import { createClient } from "@/utils/supabase/client"; interface UploadLogoProps { @@ -23,33 +24,55 @@ export default function UploadLogo({ const handleFile = async (file: File) => { if (!file.type.startsWith("image/")) { + toast.error("Please select an image file."); return; } const MAX_FILE_SIZE = 1024 * 1024; // 1MB in bytes if (file.size > MAX_FILE_SIZE) { + toast.error("Image must be smaller than 1MB."); return; } + const previousPreview = preview; setIsUploading(true); + // Show an optimistic preview while the upload is in flight. It is reverted + // below if the upload fails so the UI never shows an image that wasn't + // actually saved. + let allowReaderPreview = true; + const reader = new FileReader(); + reader.onload = (e) => { + if (!allowReaderPreview) return; + setPreview(e.target?.result as string); + }; + reader.readAsDataURL(file); + try { - // Create preview - const reader = new FileReader(); - reader.onload = (e) => { - const dataUrl = e.target?.result as string; - setPreview(dataUrl); - }; - reader.readAsDataURL(file); - - // Upload to Supabase Storage const supabase = createClient(); + + // Uploading to the avatars bucket requires an authenticated session. The + // bucket's row-level security policy only allows the `authenticated` + // role, so without a session the request is sent as `anon` and Storage + // rejects it with "new row violates row-level security policy". + const { + data: { user }, + } = await supabase.auth.getUser(); + + if (!user) { + toast.error("Please sign in to upload an image."); + allowReaderPreview = false; + setPreview(previousPreview); + return; + } + const fileExt = file.name.split(".").pop(); const fileName = `${Math.random().toString(36).substring(2)}.${fileExt}`; + const path = `${prefix}/${fileName}`; - const { data, error } = await supabase.storage + const { error } = await supabase.storage .from("avatars") - .upload(`${prefix}/${fileName}`, file, { + .upload(path, file, { cacheControl: "3600", upsert: false, }); @@ -58,16 +81,20 @@ export default function UploadLogo({ throw error; } - // Get public URL const { data: { publicUrl }, - } = supabase.storage - .from("avatars") - .getPublicUrl(`${prefix}/${fileName}`); + } = supabase.storage.from("avatars").getPublicUrl(path); + allowReaderPreview = false; + setPreview(publicUrl); onUpload?.(publicUrl); } catch (error) { console.error("Error uploading file:", error); + toast.error( + error instanceof Error ? error.message : "Failed to upload image.", + ); + allowReaderPreview = false; + setPreview(previousPreview); } finally { setIsUploading(false); } @@ -149,6 +176,12 @@ export default function UploadLogo({ )} + + {isUploading && ( +
+ Uploading... +
+ )} ); }