Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 35 additions & 6 deletions apps/cursor/src/components/company/add-company-button.tsx
Original file line number Diff line number Diff line change
@@ -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<boolean | null>(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;
Comment thread
cursor[bot] marked this conversation as resolved.
}

if (isAuthenticated === null) {
return;
}

setAddCompany({ addCompany: true, redirect });
};

return (
<Button
type="button"
variant="outline"
size="lg"
onClick={() => setAddCompany({ addCompany: true, redirect })}
>
<Button type="button" variant="outline" size="lg" onClick={handleClick}>
Add company
</Button>
);
Expand Down
63 changes: 48 additions & 15 deletions apps/cursor/src/components/upload-logo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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);
Comment thread
cursor[bot] marked this conversation as resolved.

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,
});
Expand All @@ -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);
}
Expand Down Expand Up @@ -149,6 +176,12 @@ export default function UploadLogo({
<PlusIcon className="size-4" />
</div>
)}

{isUploading && (
<div className="absolute inset-0 flex items-center justify-center rounded-lg bg-black/50 text-[10px] font-medium text-white">
Uploading...
</div>
)}
</div>
);
}