Skip to content
Merged
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
57 changes: 23 additions & 34 deletions src/components/home/HomeContent.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useState, useRef, useEffect, useCallback, createContext, useContext } from "react";
import { useState, useRef, useEffect, createContext, useContext } from "react";
import { HomePromptInput } from "./HomePromptInput";
import { DynamicTagline } from "./DynamicTagline";
import { WorkspaceGrid } from "./WorkspaceGrid";
Expand All @@ -9,9 +9,9 @@ import { FloatingWorkspaceCards } from "@/components/landing/FloatingWorkspaceCa
import { HeroGlow } from "./HeroGlow";
import { Button } from "@/components/ui/button";
import { useRouter } from "next/navigation";
import { useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import { FolderPlus } from "lucide-react";
import { useCreateWorkspace } from "@/hooks/workspace/use-create-workspace";

// Context for section visibility - allows child components to know when to focus
const SectionVisibilityContext = createContext<{
Expand All @@ -23,11 +23,11 @@ export const useSectionVisibility = () => useContext(SectionVisibilityContext);

export function HomeContent() {
const router = useRouter();
const queryClient = useQueryClient();
const [scrollY, setScrollY] = useState(0);
const [searchQuery, setSearchQuery] = useState("");
const [isCreatingWorkspace, setIsCreatingWorkspace] = useState(false);
const [heroVisible, setHeroVisible] = useState(true);

const createWorkspace = useCreateWorkspace();
const [workspacesVisible, setWorkspacesVisible] = useState(false);
const scrollRef = useRef<HTMLDivElement>(null);
const heroRef = useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -74,37 +74,26 @@ export function HomeContent() {
return () => observer.disconnect();
}, []);

const handleCreateBlankWorkspace = async () => {
const handleCreateBlankWorkspace = () => {
// Guard against multiple rapid clicks
if (isCreatingWorkspace) return;

setIsCreatingWorkspace(true);
try {
const createRes = await fetch("/api/workspaces", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: "Blank Workspace",
icon: null,
color: null,
}),
});
if (!createRes.ok) {
const err = await createRes.json().catch(() => ({}));
throw new Error(err.error || "Failed to create workspace");
if (createWorkspace.isPending) return;

createWorkspace.mutate(
{
name: "Blank Workspace",
icon: null,
color: null,
},
{
onSuccess: ({ workspace }) => {
router.push(`/workspace/${workspace.slug}`);
},
onError: (err) => {
const msg = err instanceof Error ? err.message : "Something went wrong";
toast.error("Could not create workspace", { description: msg });
},
}
const { workspace } = (await createRes.json()) as { workspace: { slug: string } };

// Invalidate workspaces cache so the new workspace is available immediately
await queryClient.invalidateQueries({ queryKey: ['workspaces'] });

router.push(`/workspace/${workspace.slug}`);
} catch (err) {
const msg = err instanceof Error ? err.message : "Something went wrong";
toast.error("Could not create workspace", { description: msg });
} finally {
setIsCreatingWorkspace(false);
}
);
};

return (
Expand Down Expand Up @@ -155,7 +144,7 @@ export function HomeContent() {
variant="ghost"
size="sm"
onClick={handleCreateBlankWorkspace}
disabled={isCreatingWorkspace}
disabled={createWorkspace.isPending}
className="text-sm text-white/70 hover:text-white hover:bg-white/5 transition-all duration-200 gap-2 disabled:opacity-50"
>
<FolderPlus className="h-4 w-4" />
Expand Down
70 changes: 20 additions & 50 deletions src/components/home/HomePromptInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

import { useState, useRef, useMemo, useEffect } from "react";
import { useRouter } from "next/navigation";
import { useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import { useCreateWorkspaceFromPrompt } from "@/hooks/workspace/use-create-workspace";
import { ArrowUp, Loader2 } from "lucide-react";
import { cn } from "@/lib/utils";
import { Input } from "@/components/ui/input";
Expand Down Expand Up @@ -54,11 +54,11 @@ interface HomePromptInputProps {

export function HomePromptInput({ shouldFocus }: HomePromptInputProps) {
const router = useRouter();
const queryClient = useQueryClient();
const [value, setValue] = useState("");
const [isLoading, setIsLoading] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const typingKeyRef = useRef(0);

const createFromPrompt = useCreateWorkspaceFromPrompt();

// Shuffle options with random start for variety
const shuffledOptions = useMemo(() => {
Expand Down Expand Up @@ -88,51 +88,21 @@ export function HomePromptInput({ shouldFocus }: HomePromptInputProps) {
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const prompt = value.trim();
if (!prompt || isLoading) return;

setIsLoading(true);
try {
const titleRes = await fetch("/api/workspaces/generate-title", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt }),
});
if (!titleRes.ok) {
const err = await titleRes.json().catch(() => ({}));
throw new Error(err.error || "Failed to generate title");
}
const { title, icon, color } = (await titleRes.json()) as { title: string; icon?: string; color?: string };

const createRes = await fetch("/api/workspaces", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: title,
icon: icon || null,
color: color || null,
template: "getting_started", // Auto-include sample content (quiz/flashcards) for home prompt (magic feeling)
}),
});
if (!createRes.ok) {
const err = await createRes.json().catch(() => ({}));
throw new Error(err.error || "Failed to create workspace");
}
const { workspace } = (await createRes.json()) as { workspace: { slug: string } };

// Invalidate workspaces cache so the new workspace is available immediately
await queryClient.invalidateQueries({ queryKey: ['workspaces'] });

// Reset typing animation by changing key
typingKeyRef.current += 1;
router.push(
`/workspace/${workspace.slug}?createFrom=${encodeURIComponent(prompt)}`
);
} catch (err) {
const msg = err instanceof Error ? err.message : "Something went wrong";
toast.error("Could not create workspace", { description: msg });
} finally {
setIsLoading(false);
}
if (!prompt || createFromPrompt.isLoading) return;

createFromPrompt.mutate(prompt, {
template: "getting_started", // Auto-include sample content (quiz/flashcards) for home prompt (magic feeling)
onSuccess: (workspace) => {
// Reset typing animation by changing key
typingKeyRef.current += 1;
router.push(
`/workspace/${workspace.slug}?createFrom=${encodeURIComponent(prompt)}`
);
},
onError: (err) => {
toast.error("Could not create workspace", { description: err.message });
},
});
Comment on lines 88 to +105

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Prevent unhandled promise rejections from mutate().
useCreateWorkspaceFromPrompt.mutate rethrows on error, so calling it without await/catch can surface an unhandled rejection despite onError handling.

✅ Suggested fix
-    createFromPrompt.mutate(prompt, {
-      template: "getting_started", // Auto-include sample content (quiz/flashcards) for home prompt (magic feeling)
-      onSuccess: (workspace) => {
-        // Reset typing animation by changing key
-        typingKeyRef.current += 1;
-        router.push(
-          `/workspace/${workspace.slug}?createFrom=${encodeURIComponent(prompt)}`
-        );
-      },
-      onError: (err) => {
-        toast.error("Could not create workspace", { description: err.message });
-      },
-    });
+    try {
+      await createFromPrompt.mutateAsync(prompt, {
+        template: "getting_started", // Auto-include sample content (quiz/flashcards) for home prompt (magic feeling)
+        onSuccess: (workspace) => {
+          // Reset typing animation by changing key
+          typingKeyRef.current += 1;
+          router.push(
+            `/workspace/${workspace.slug}?createFrom=${encodeURIComponent(prompt)}`
+          );
+        },
+        onError: (err) => {
+          toast.error("Could not create workspace", { description: err.message });
+        },
+      });
+    } catch {
+      // onError already surfaced a toast; swallow to avoid unhandled rejection
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const prompt = value.trim();
if (!prompt || isLoading) return;
setIsLoading(true);
try {
const titleRes = await fetch("/api/workspaces/generate-title", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt }),
});
if (!titleRes.ok) {
const err = await titleRes.json().catch(() => ({}));
throw new Error(err.error || "Failed to generate title");
}
const { title, icon, color } = (await titleRes.json()) as { title: string; icon?: string; color?: string };
const createRes = await fetch("/api/workspaces", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: title,
icon: icon || null,
color: color || null,
template: "getting_started", // Auto-include sample content (quiz/flashcards) for home prompt (magic feeling)
}),
});
if (!createRes.ok) {
const err = await createRes.json().catch(() => ({}));
throw new Error(err.error || "Failed to create workspace");
}
const { workspace } = (await createRes.json()) as { workspace: { slug: string } };
// Invalidate workspaces cache so the new workspace is available immediately
await queryClient.invalidateQueries({ queryKey: ['workspaces'] });
// Reset typing animation by changing key
typingKeyRef.current += 1;
router.push(
`/workspace/${workspace.slug}?createFrom=${encodeURIComponent(prompt)}`
);
} catch (err) {
const msg = err instanceof Error ? err.message : "Something went wrong";
toast.error("Could not create workspace", { description: msg });
} finally {
setIsLoading(false);
}
if (!prompt || createFromPrompt.isLoading) return;
createFromPrompt.mutate(prompt, {
template: "getting_started", // Auto-include sample content (quiz/flashcards) for home prompt (magic feeling)
onSuccess: (workspace) => {
// Reset typing animation by changing key
typingKeyRef.current += 1;
router.push(
`/workspace/${workspace.slug}?createFrom=${encodeURIComponent(prompt)}`
);
},
onError: (err) => {
toast.error("Could not create workspace", { description: err.message });
},
});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const prompt = value.trim();
if (!prompt || createFromPrompt.isLoading) return;
try {
await createFromPrompt.mutateAsync(prompt, {
template: "getting_started", // Auto-include sample content (quiz/flashcards) for home prompt (magic feeling)
onSuccess: (workspace) => {
// Reset typing animation by changing key
typingKeyRef.current += 1;
router.push(
`/workspace/${workspace.slug}?createFrom=${encodeURIComponent(prompt)}`
);
},
onError: (err) => {
toast.error("Could not create workspace", { description: err.message });
},
});
} catch {
// onError already surfaced a toast; swallow to avoid unhandled rejection
}
🤖 Prompt for AI Agents
In `@src/components/home/HomePromptInput.tsx` around lines 88 - 105, handleSubmit
uses createFromPrompt.mutate which can rethrow and cause unhandled promise
rejections; change to using the promise-based API and await it inside a
try/catch: call createFromPrompt.mutateAsync(prompt, { template:
"getting_started", onSuccess: ..., onError: ... }) and wrap that await in
try/catch in handleSubmit so any thrown error is caught and handled (e.g., call
toast.error in catch) while keeping the existing onSuccess/onError handlers;
reference handleSubmit and createFromPrompt.mutate/mutateAsync to locate the
change.

};

return (
Expand Down Expand Up @@ -230,7 +200,7 @@ export function HomePromptInput({ shouldFocus }: HomePromptInputProps) {
{/* Submit arrow */}
<button
type="submit"
disabled={!value.trim() || isLoading}
disabled={!value.trim() || createFromPrompt.isLoading}
onClick={(e) => e.stopPropagation()}
className={cn(
"absolute right-3 top-1/2 -translate-y-1/2 z-20",
Expand All @@ -239,7 +209,7 @@ export function HomePromptInput({ shouldFocus }: HomePromptInputProps) {
"hover:opacity-80"
)}
>
{isLoading ? (
{createFromPrompt.isLoading ? (
<Loader2 className="h-6 w-6 text-white animate-spin" />
) : (
<ArrowUp className="h-6 w-6 text-white" />
Expand Down
Loading