diff --git a/next.config.js b/next.config.js index 56fc17a..91ef62f 100644 --- a/next.config.js +++ b/next.config.js @@ -1,14 +1,6 @@ /** @type {import('next').NextConfig} */ const nextConfig = { - output: "export", reactStrictMode: true, - images: { - loader: "akamai", - path: "", - unoptimized: true, - }, - basePath: "", - assetPrefix: "", }; module.exports = nextConfig; diff --git a/src/app/auth/callback/page.tsx b/src/app/auth/callback/page.tsx index 4ca868d..65fa4bf 100644 --- a/src/app/auth/callback/page.tsx +++ b/src/app/auth/callback/page.tsx @@ -2,44 +2,50 @@ import { useEffect, useState } from "react"; import { useRouter } from "next/navigation"; -import { supabase } from "@/lib/supabase"; +import { supabase } from "@/database/supabase"; import Image from "next/image"; import logo from "@/public/home/cookCraftLogo.webp"; const AuthCallbackPage = () => { const router = useRouter(); - const [error, setError] = useState(null); + const [errorMessage, setErrorMessage] = useState(null); useEffect(() => { - const handleCallback = async () => { + const handleAuthCallback = async () => { try { - const hashParams = new URLSearchParams( - window.location.hash.substring(1), - ); + const urlHash = window.location.hash.substring(1); + const hashParams = new URLSearchParams(urlHash); + const accessToken = hashParams.get("access_token"); const refreshToken = hashParams.get("refresh_token"); if (accessToken && refreshToken) { - const { error } = await supabase.auth.setSession({ + const sessionResult = await supabase.auth.setSession({ access_token: accessToken, refresh_token: refreshToken, }); - if (error) throw error; + if (sessionResult.error) { + throw sessionResult.error; + } router.push("/profile"); } else { throw new Error("No tokens found in URL"); } } catch (err) { - setError(err instanceof Error ? err.message : "Authentication failed"); + if (err instanceof Error) { + setErrorMessage(err.message); + } else { + setErrorMessage("Authentication failed"); + } } }; - handleCallback(); + handleAuthCallback(); }, [router]); - if (error) { + if (errorMessage) { return (
CookCraft Logo @@ -47,7 +53,7 @@ const AuthCallbackPage = () => {

Authentication Error

-

{error}

+

{errorMessage}

+
+ + ); + })} + + )} + ); }; -export default History; +export default HistoryPage; diff --git a/src/app/inventory/page.tsx b/src/app/inventory/page.tsx new file mode 100644 index 0000000..bfb27f1 --- /dev/null +++ b/src/app/inventory/page.tsx @@ -0,0 +1,372 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { + getIngredients, + createIngredient, + updateIngredient, + deleteIngredient, +} from "@/database/api/ingredients"; +import { Ingredient } from "@/types/database"; +import { INGREDIENT_CATEGORIES, UNITS } from "@/types/database"; + +const InventoryPage = () => { + const [ingredients, setIngredients] = useState([]); + const [loading, setLoading] = useState(true); + const [showForm, setShowForm] = useState(false); + const [editingId, setEditingId] = useState(null); + const [searchTerm, setSearchTerm] = useState(""); + const [filterCategory, setFilterCategory] = useState(""); + const [expandedId, setExpandedId] = useState(null); + + const [formData, setFormData] = useState({ + name: "", + quantity: 0, + unit: "pieces", + category: "Other", + notes: "", + }); + + useEffect(() => { + loadIngredients(); + }, []); + + const loadIngredients = async () => { + try { + const data = await getIngredients(); + setIngredients(data); + } catch (error) { + console.error(error); + } finally { + setLoading(false); + } + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + try { + if (editingId) { + await updateIngredient(editingId, formData); + } else { + await createIngredient(formData); + } + await loadIngredients(); + resetForm(); + } catch (error) { + console.error(error); + } + }; + + const handleEdit = (ingredient: Ingredient) => { + setEditingId(ingredient.id); + setFormData({ + name: ingredient.name, + quantity: Number(ingredient.quantity), + unit: ingredient.unit, + category: ingredient.category || "Other", + notes: ingredient.notes || "", + }); + setShowForm(true); + setExpandedId(null); + }; + + const handleDelete = async (id: string) => { + if (confirm("Delete this ingredient?")) { + try { + await deleteIngredient(id); + await loadIngredients(); + setExpandedId(null); + } catch (error) { + console.error(error); + } + } + }; + + const handleQuickAdd = async (ingredient: Ingredient) => { + try { + await updateIngredient(ingredient.id, { + quantity: Number(ingredient.quantity) + 1, + }); + await loadIngredients(); + } catch (error) { + console.error(error); + } + }; + + const resetForm = () => { + setFormData({ + name: "", + quantity: 0, + unit: "pieces", + category: "Other", + notes: "", + }); + setEditingId(null); + setShowForm(false); + }; + + const filteredIngredients = ingredients.filter((ing) => { + const matchesSearch = ing.name + .toLowerCase() + .includes(searchTerm.toLowerCase()); + const matchesCategory = filterCategory + ? ing.category === filterCategory + : true; + return matchesSearch && matchesCategory; + }); + + const groupedIngredients = filteredIngredients.reduce( + (acc, ing) => { + const category = ing.category || "Other"; + if (!acc[category]) acc[category] = []; + acc[category].push(ing); + return acc; + }, + {} as Record, + ); + + if (loading) { + return ( +
+
+

Loading...

+
+ ); + } + + return ( +
+
+
+

+ My Inventory +

+ +
+ + {showForm && ( +
+

+ {editingId ? "Edit Ingredient" : "Add Ingredient"} +

+
+
+ + + setFormData({ ...formData, name: e.target.value }) + } + required + className="border-cookcraft-olive mt-1 w-full rounded-2xl border-3 p-3" + /> +
+ +
+
+ + + setFormData({ + ...formData, + quantity: parseFloat(e.target.value) || 0, + }) + } + required + className="border-cookcraft-olive mt-1 w-full rounded-2xl border-3 p-3" + /> +
+ +
+ + +
+
+ +
+ + +
+ +
+ + + setFormData({ ...formData, notes: e.target.value }) + } + className="border-cookcraft-olive mt-1 w-full rounded-2xl border-3 p-3" + /> +
+ +
+ + +
+
+
+ )} + +
+ setSearchTerm(e.target.value)} + className="border-cookcraft-olive flex-1 rounded-2xl border-3 p-3" + /> + +
+ + {ingredients.length === 0 ? ( +
+

+ No ingredients yet. Add your first one! +

+
+ ) : filteredIngredients.length === 0 ? ( +
+

+ No ingredients match your search. +

+
+ ) : ( +
+ {Object.entries(groupedIngredients).map(([category, items]) => ( +
+

+ {category} +

+
+ {items.map((ingredient) => { + const isExpanded = expandedId === ingredient.id; + return ( +
+
+ setExpandedId(isExpanded ? null : ingredient.id) + } + > +

+ {ingredient.name} +

+

+ {ingredient.quantity} {ingredient.unit} +

+
+ + {isExpanded && ingredient.notes && ( +

+ {ingredient.notes} +

+ )} + +
+ + +
+ + {isExpanded && ( +
+ +
+ )} +
+ ); + })} +
+
+ ))} +
+ )} +
+
+ ); +}; + +export default InventoryPage; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 25977cd..85283ff 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,5 @@ import "./globals.css"; import { Roboto } from "next/font/google"; -import { ReactQueryClientProvider } from "@/utils/react-query"; import { AuthProvider } from "@/contexts/AuthContext"; import NavBar from "@/components/NavBar"; import Footer from "@/components/Footer"; @@ -26,7 +25,7 @@ export default function RootLayout({ children }: LayoutProps) { - {children} + {children}