diff --git a/app/(authenticated)/dashboard/page.tsx b/app/(authenticated)/dashboard/page.tsx index 7da3136..cc2fa29 100644 --- a/app/(authenticated)/dashboard/page.tsx +++ b/app/(authenticated)/dashboard/page.tsx @@ -80,6 +80,11 @@ export default function DashboardPage() { errors: string[]; warnings: string[]; } | null>(null); + const [categoryOptions, setCategoryOptions] = useState>([]); + const [categoryModal, setCategoryModal] = useState<{ toolId: string; toolName: string } | null>(null); + const [selectedCategoryIds, setSelectedCategoryIds] = useState([]); + const [savingCategories, setSavingCategories] = useState(false); + const [categoryError, setCategoryError] = useState(null); useEffect(() => { // Get auth token from sessionStorage (set by layout) @@ -116,6 +121,20 @@ export default function DashboardPage() { })(); }, []); + // Fetch category options once for the "Edit categories" modal + useEffect(() => { + (async () => { + try { + const response = await fetch("/api/categories"); + if (!response.ok) throw new Error("Failed to fetch categories"); + const data = await response.json(); + setCategoryOptions(Array.isArray(data) ? data : []); + } catch (error) { + console.error("Error fetching categories:", error); + } + })(); + }, []); + // Close the "More" dropdown on scroll or resize to avoid stale fixed positioning useEffect(() => { if (openMoreMenuForToolId === null) return; @@ -221,6 +240,61 @@ export default function DashboardPage() { } }; + const openCategoryModal = (tool: Tool) => { + setCategoryModal({ toolId: tool.id, toolName: tool.name }); + setSelectedCategoryIds(tool.categories?.map((cat) => cat.id) || []); + setCategoryError(null); + }; + + const handleCategoryToggle = (categoryId: number) => { + setSelectedCategoryIds((prev) => { + if (prev.includes(categoryId)) { + return prev.filter((id) => id !== categoryId); + } else if (prev.length < 3) { + return [...prev, categoryId]; + } + return prev; + }); + }; + + const handleAssignCategories = async () => { + if (!categoryModal || !authToken) return; + + if (selectedCategoryIds.length === 0) { + setCategoryError("Please select at least one category"); + return; + } + + setSavingCategories(true); + setCategoryError(null); + try { + const response = await fetch("/api/tools/update-categories", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${authToken}`, + }, + body: JSON.stringify({ toolId: categoryModal.toolId, categoryIds: selectedCategoryIds }), + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || "Failed to update categories"); + } + + const assigned: Array<{ id: number; name: string }> = data.categories || []; + setTools((prevTools) => prevTools.map((tool) => (tool.id === categoryModal.toolId ? { ...tool, categories: assigned } : tool))); + setCategoryModal(null); + setSelectedCategoryIds([]); + } catch (error) { + console.error("Error updating categories:", error); + setCategoryError(error instanceof Error ? error.message : "Failed to update categories. Please try again."); + } finally { + setSavingCategories(false); + } + }; + // Filter tools based on view mode. Intakes only appear in "My Tools". const filteredTools = viewMode === "my" @@ -718,6 +792,26 @@ export default function DashboardPage() { } }} > + + +
+ {categoryError && ( +
{categoryError}
+ )} + {categoryOptions.length === 0 ? ( +

No categories available. Please contact an administrator.

+ ) : ( + <> +
+ {categoryOptions.map((category) => { + const isSelected = selectedCategoryIds.includes(category.id); + const isDisabled = savingCategories || (selectedCategoryIds.length >= 3 && !isSelected); + return ( + + ); + })} +
+

Select up to 3 categories that best describe your tool ({selectedCategoryIds.length}/3 selected)

+ + )} +
+
+ + +
+ + + )} ); } diff --git a/app/api/tools/update-categories/route.ts b/app/api/tools/update-categories/route.ts new file mode 100644 index 0000000..13da0d4 --- /dev/null +++ b/app/api/tools/update-categories/route.ts @@ -0,0 +1,138 @@ +import { createClient } from "@supabase/supabase-js"; +import { NextRequest, NextResponse } from "next/server"; + +// Create Supabase client with service role for server-side operations +function getSupabaseClient() { + const supabaseUrl = process.env.SUPABASE_URL; + const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY; + + if (!supabaseUrl || !supabaseServiceKey) { + return null; + } + + return createClient(supabaseUrl, supabaseServiceKey); +} + +interface UpdateCategoriesRequest { + toolId: string; + categoryIds: number[]; +} + +export async function POST(request: NextRequest) { + try { + const supabase = getSupabaseClient(); + + if (!supabase) { + return NextResponse.json({ error: "Database connection not configured" }, { status: 500 }); + } + + // Verify user is authenticated + const authHeader = request.headers.get("authorization"); + let userId: string | null = null; + + if (authHeader?.startsWith("Bearer ")) { + const token = authHeader.slice(7); + const { + data: { user }, + error: authError, + } = await supabase.auth.getUser(token); + + if (!authError && user) { + userId = user.id; + } else { + return NextResponse.json({ error: "Unauthorized. Valid user token required." }, { status: 401 }); + } + } + + if (!userId) { + return NextResponse.json({ error: "Unauthorized. Please sign in." }, { status: 401 }); + } + + // Parse request body + const body = (await request.json()) as UpdateCategoriesRequest; + const { toolId, categoryIds } = body; + + if (!toolId) { + return NextResponse.json({ error: "toolId is required" }, { status: 400 }); + } + + if (!categoryIds || !Array.isArray(categoryIds) || categoryIds.length === 0) { + return NextResponse.json({ error: "At least one category is required" }, { status: 400 }); + } + + const uniqueCategoryIds = Array.from(new Set(categoryIds)); + + if (uniqueCategoryIds.length > 3) { + return NextResponse.json({ error: "Please select no more than 3 categories" }, { status: 400 }); + } + + // Verify the tool exists and belongs to the user + const { data: tool, error: fetchError } = await supabase.from("tools").select("id, user_id").eq("id", toolId).single(); + + if (fetchError || !tool) { + return NextResponse.json({ error: "Tool not found" }, { status: 404 }); + } + + if (tool.user_id !== userId) { + return NextResponse.json({ error: "You do not have permission to update this tool" }, { status: 403 }); + } + + // Validate that all provided category IDs exist + const { data: existingCategories, error: categoriesLookupError } = await supabase + .from("categories") + .select("id, name") + .in("id", uniqueCategoryIds); + + if (categoriesLookupError || !existingCategories) { + console.error("Error validating categories:", categoriesLookupError); + return NextResponse.json({ error: "Failed to validate categories. Please try again." }, { status: 500 }); + } + + const validCategoryIds = new Set(existingCategories.map((c) => c.id)); + const invalidCount = uniqueCategoryIds.filter((id) => !validCategoryIds.has(id)).length; + + if (invalidCount > 0) { + return NextResponse.json( + { + error: `${invalidCount} selected ${invalidCount === 1 ? "category is" : "categories are"} invalid. Please try again with valid categories.`, + }, + { status: 400 }, + ); + } + + // Replace existing category relationships so owners can add/remove freely + const { error: deleteError } = await supabase.from("tool_categories").delete().eq("tool_id", toolId); + + if (deleteError) { + console.error("Error clearing tool categories:", deleteError); + return NextResponse.json({ error: "Failed to update tool categories. Please try again." }, { status: 500 }); + } + + const categoryRelations = uniqueCategoryIds.map((categoryId) => ({ + tool_id: toolId, + category_id: categoryId, + })); + + const { error: insertError } = await supabase.from("tool_categories").insert(categoryRelations); + + if (insertError) { + console.error("Error inserting tool categories:", insertError); + return NextResponse.json({ error: "Failed to save tool categories. Please try again." }, { status: 500 }); + } + + const categoryById = new Map(existingCategories.map((c) => [c.id, c])); + const categories = uniqueCategoryIds + .map((id) => categoryById.get(id)) + .filter((c): c is { id: number; name: string } => Boolean(c)) + .map((c) => ({ id: c.id, name: c.name })); + + return NextResponse.json({ + success: true, + message: "Categories updated successfully", + categories, + }); + } catch (error) { + console.error("Error updating tool categories:", error); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } +} diff --git a/lib/mock-tools.ts b/lib/mock-tools.ts index 76b7d08..a4615f3 100644 --- a/lib/mock-tools.ts +++ b/lib/mock-tools.ts @@ -21,7 +21,7 @@ export const mockTools: MockTool[] = [ description: "Manage your Power Platform solutions with ease. Export, import, and version control your solutions.", icon: "📦", contributors: ["Power Platform ToolBox"], - categories: ["Solutions"], + categories: [], downloads: 1250, rating: 4.8, mau: 320,