Skip to content
Draft
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
195 changes: 195 additions & 0 deletions app/admin/AddProduct.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
"use client"

import { useState, type ChangeEvent } from "react"
import type { Bike } from "@/utils/getBike"
import type React from "react" // Import React
import uploadImage from "./image";
import { useRouter } from "next/navigation";

export default function AddProduct() {
const router = useRouter();
const [newBike, setNewBike] = useState<Omit<Bike, "bike_id">>({
name: "",
description: "",
image: null,
amount_stocked: 0,
rental_rate: 0,
sell_price: 0,
damage_rate: 0,
for_rent: false,
})
const [uploading, setUploading] = useState(false)

const handleChange = (field: keyof Omit<Bike, "bike_id">, value: string | number | boolean) => {
setNewBike((prev) => ({ ...prev, [field]: value }))
}

const handleUpload = (e: ChangeEvent<HTMLInputElement>) => {
setUploading(true)
uploadImage(e).then((signedUrl) => {
if (signedUrl) {
setNewBike((prev) => ({ ...prev, image: signedUrl }))
} else {
console.error("Error uploading image")
}
setUploading(false)
})
}

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
console.log("New bike to be added:", newBike)
// Here you would typically send the data to your backend API
// Reset the form after submission
try {
const response = await fetch("/api/add-bike", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(newBike),
})
if (!response.ok) {
throw new Error("Failed to add bike")
} else {
console.log("Bike added successfully")
router.refresh()
}
} catch (error) {
console.error("Error adding bike:", error)
}
setNewBike({
name: "",
description: "",
image: null,
amount_stocked: 0,
rental_rate: 0,
sell_price: 0,
damage_rate: 0,
for_rent: false,
})
}

return (
<div id="add-product" className="bg-green-50 p-6 rounded-lg">
<h2 className="text-2xl font-bold mb-4 text-green-700">Add New Bike</h2>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
Name
</label>
<input
id="name"
type="text"
value={newBike.name}
onChange={(e) => handleChange("name", e.target.value)}
className="mt-1 block w-full px-3 py-2 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-green-500 focus:border-green-500"
required
/>
</div>
<div>
<label htmlFor="description" className="block text-sm font-medium text-gray-700">
Description
</label>
<textarea
id="description"
value={newBike.description}
onChange={(e) => handleChange("description", e.target.value)}
className="mt-1 block w-full px-3 py-2 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-green-500 focus:border-green-500"
required
/>
</div>
<div>
<label htmlFor="image" className="block text-sm font-medium text-gray-700">
Image
</label>
<input
id="image"
type="file"
accept="image/*"
onChange={handleUpload}
className="mt-1 block w-full px-3 py-2 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-green-500 focus:border-green-500"
/>
{uploading && <p className="mt-2 text-sm text-gray-500">Uploading...</p>}
{newBike.image && <p className="mt-2 text-sm text-gray-500">Image uploaded: {newBike.image}</p>}
</div>
<div>
<label htmlFor="amount_stocked" className="block text-sm font-medium text-gray-700">
Amount Stocked
</label>
<input
id="amount_stocked"
type="number"
value={newBike.amount_stocked}
onChange={(e) => handleChange("amount_stocked", Number(e.target.value))}
className="mt-1 block w-full px-3 py-2 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-green-500 focus:border-green-500"
required
/>
</div>
<div>
<label htmlFor="rental_rate" className="block text-sm font-medium text-gray-700">
Rental Rate
</label>
<input
id="rental_rate"
type="number"
step="0.01"
value={newBike.rental_rate}
onChange={(e) => handleChange("rental_rate", Number(e.target.value))}
className="mt-1 block w-full px-3 py-2 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-green-500 focus:border-green-500"
required
/>
</div>
<div>
<label htmlFor="sell_price" className="block text-sm font-medium text-gray-700">
Sell Price
</label>
<input
id="sell_price"
type="number"
step="0.01"
value={newBike.sell_price}
onChange={(e) => handleChange("sell_price", Number(e.target.value))}
className="mt-1 block w-full px-3 py-2 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-green-500 focus:border-green-500"
required
/>
</div>
<div>
<label htmlFor="damage_rate" className="block text-sm font-medium text-gray-700">
Damage Rate
</label>
<input
id="damage_rate"
type="number"
step="0.01"
value={newBike.damage_rate}
onChange={(e) => handleChange("damage_rate", Number(e.target.value))}
className="mt-1 block w-full px-3 py-2 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-green-500 focus:border-green-500"
required
/>
</div>
<div>
<label htmlFor="for_rent" className="block text-sm font-medium text-gray-700">
For Rent
</label>
<select
id="for_rent"
value={newBike.for_rent.toString()}
onChange={(e) => handleChange("for_rent", e.target.value === "true")}
className="mt-1 block w-full px-3 py-2 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-green-500 focus:border-green-500"
>
<option value="true">Yes</option>
<option value="false">No</option>
</select>
</div>
<button
type="submit"
className="w-full bg-green-700 text-white px-4 py-2 rounded-md hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2"
>
Add New Bike
</button>
</form>
</div>
)
}

184 changes: 184 additions & 0 deletions app/admin/Inventory.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
"use client"

import { ChangeEvent, useEffect, useState } from "react"
import type { Bike } from "@/utils/getBike"
import { createClient } from "@/utils/supabase/client";
import uploadImage from "./image";

export default function Inventory() {
const [inventoryData, setInventoryData] = useState<Bike[]>([])
const [editingId, setEditingId] = useState<number | null>(null)
const [uploading, setUploading] = useState(false)

const supabase = createClient()

Comment on lines +8 to +14
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

The supabase client is created outside the useEffect hook and will not be recreated on component re-renders. According to Next.js and Supabase best practices, the client should typically be created once at the module level or inside hooks where it's used. However, since this is used in a useEffect, consider moving it inside or verify this pattern is intentional.

Suggested change
export default function Inventory() {
const [inventoryData, setInventoryData] = useState<Bike[]>([])
const [editingId, setEditingId] = useState<number | null>(null)
const [uploading, setUploading] = useState(false)
const supabase = createClient()
const supabase = createClient()
export default function Inventory() {
const [inventoryData, setInventoryData] = useState<Bike[]>([])
const [editingId, setEditingId] = useState<number | null>(null)
const [uploading, setUploading] = useState(false)

Copilot uses AI. Check for mistakes.
useEffect(() => {
async function fetchInventory() {
const { data, error } = await supabase.from("bikes").select("*").order("bike_id")

if (error) {
console.error("Error fetching inventory:", error.message)
} else if (data) {
setInventoryData(data)
}
}
fetchInventory()
}, [])
Comment on lines +15 to +26
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

The useEffect hook is missing supabase from its dependency array. This violates the React rules of hooks and could lead to stale closures or unexpected behavior. Either add supabase to the dependency array or create the supabase client inside the useEffect.

Copilot uses AI. Check for mistakes.

const handleUpload = async (e: ChangeEvent<HTMLInputElement>, id: number) => {
setUploading(true)

const signedUrl = await uploadImage(e)
if (signedUrl) {
handleChange(id, "image", signedUrl)
} else {
console.error("Error uploading image")
Comment on lines +31 to +35
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

The setUploading(false) call is missing after the upload completes in the handleUpload function. Once the signed URL is obtained or an error occurs, the uploading state should be set back to false. Currently, the uploading indicator will remain visible indefinitely after an upload.

Suggested change
const signedUrl = await uploadImage(e)
if (signedUrl) {
handleChange(id, "image", signedUrl)
} else {
console.error("Error uploading image")
try {
const signedUrl = await uploadImage(e)
if (signedUrl) {
handleChange(id, "image", signedUrl)
} else {
console.error("Error uploading image")
}
} catch (error) {
console.error("Unexpected error during image upload:", error)
} finally {
setUploading(false)

Copilot uses AI. Check for mistakes.
}
}

const handleChange = async (id: number, field: keyof Bike, value: string | number | boolean) => {
setInventoryData(inventoryData.map((item) => (item.bike_id === id ? { ...item, [field]: value } : item)))
}

const handleSave = async (id: number) => {
await supabase.from("bikes").upsert(inventoryData).eq("bike_id", id)
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

The handleSave function has a critical bug in the upsert call. The .eq() method should be called on the query builder before .upsert(), not after. The correct pattern is to use .upsert() with the onConflict option or to call .update() with .eq(). Currently, this will attempt to upsert all items in the inventoryData array rather than just the one being edited. The correct approach would be: await supabase.from("bikes").update(inventoryData.find(item => item.bike_id === id)).eq("bike_id", id)

Suggested change
await supabase.from("bikes").upsert(inventoryData).eq("bike_id", id)
const bikeToUpdate = inventoryData.find((item) => item.bike_id === id)
if (bikeToUpdate) {
await supabase.from("bikes").update(bikeToUpdate).eq("bike_id", id)
}

Copilot uses AI. Check for mistakes.
console.log(inventoryData)
setEditingId(null)
}

const handleDelete = async (id: number) => {
await supabase.from("bikes").delete().eq("bike_id", id)
setInventoryData(inventoryData.filter((item) => item.bike_id !== id))
Comment on lines +50 to +51
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

The delete operation removes the item from local state before confirming the database operation succeeded. If the database delete fails, the item will still be removed from the UI, creating an inconsistency. The state update should only happen after confirming the database operation was successful (by checking the error response).

Suggested change
await supabase.from("bikes").delete().eq("bike_id", id)
setInventoryData(inventoryData.filter((item) => item.bike_id !== id))
const { error } = await supabase.from("bikes").delete().eq("bike_id", id)
if (error) {
console.error("Error deleting bike:", error.message)
return
}
setInventoryData((prevInventory) =>
prevInventory.filter((item) => item.bike_id !== id)
)

Copilot uses AI. Check for mistakes.
}
Comment on lines +43 to +52
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

There's no error handling for the database operations in handleSave and handleDelete. If the database operation fails, the user won't be notified, but the local state will still be updated (in the case of handleDelete) or the editing state will be cleared (in handleSave). This can lead to inconsistency between the UI and database state. Add error handling and show appropriate feedback to the user.

Copilot uses AI. Check for mistakes.

return (
<div id="inventory" className="overflow-x-auto">
<h2 className="text-2xl font-bold mb-4 text-green-700">Bike Inventory</h2>
<table className="min-w-full bg-white border border-gray-300">
<thead>
<tr className="bg-gray-100">
<th className="py-2 px-4 border-b">ID</th>
<th className="py-2 px-4 border-b">Name</th>
<th className="py-2 px-4 border-b">Description</th>
<th className="py-2 px-4 border-b">Image</th>
<th className="py-2 px-4 border-b">Stock</th>
<th className="py-2 px-4 border-b">Rental Rate</th>
<th className="py-2 px-4 border-b">Sell Price</th>
<th className="py-2 px-4 border-b">Actions</th>
</tr>
</thead>
<tbody>
{inventoryData.map((bike) => (
<tr key={bike.bike_id}>
<td className="py-2 px-4 border-b">{bike.bike_id}</td>
<td className="py-2 px-4 border-b">
{editingId === bike.bike_id ? (
<input
type="text"
value={bike.name}
onChange={(e) => handleChange(bike.bike_id, "name", e.target.value)}
className="w-full p-1 border rounded"
/>
) : (
bike.name
)}
</td>
<td className="py-2 px-4 border-b">
{editingId === bike.bike_id ? (
<textarea
value={bike.description}
onChange={(e) => handleChange(bike.bike_id, "description", e.target.value)}
className="w-full p-1 border rounded"
/>
) : (
bike.description
)}
</td>
<td className="py-2 px-4 border-b">
{editingId === bike.bike_id ? (
<div>
<input
type="file"
accept="image/*"
onChange={(e) => handleUpload(e, bike.bike_id)}
className="w-full p-1 border rounded"
/>
{uploading && <p className="mt-2 text-sm text-gray-500">Uploading...</p>}
{bike.image && <p className="mt-2 text-sm text-gray-500">Current image:
<img src={bike.image} alt={bike.name} className="w-16 h-16" /></p>}
</div>
) : bike.image ? (
<img src={bike.image} alt={bike.name} className="w-32 h-32 object-cover" />
) : (
"No image"
)}
</td>
<td className="py-2 px-4 border-b">
{editingId === bike.bike_id ? (
<input
type="number"
value={bike.amount_stocked}
onChange={(e) => handleChange(bike.bike_id, "amount_stocked", Number(e.target.value))}
className="w-20 p-1 border rounded"
/>
) : (
bike.amount_stocked
)}
</td>
<td className="py-2 px-4 border-b">
{editingId === bike.bike_id ? (
<input
type="number"
step="0.01"
value={bike.rental_rate}
onChange={(e) => handleChange(bike.bike_id, "rental_rate", Number(e.target.value))}
className="w-20 p-1 border rounded"
/>
) : (
`$${bike.rental_rate.toFixed(2)}`
)}
</td>
<td className="py-2 px-4 border-b">
{editingId === bike.bike_id ? (
<input
type="number"
step="0.01"
value={bike.sell_price}
onChange={(e) => handleChange(bike.bike_id, "sell_price", Number(e.target.value))}
className="w-20 p-1 border rounded"
/>
) : (
`$${bike.sell_price.toFixed(2)}`
)}
</td>
<td className="py-2 px-4 border-b">
{editingId === bike.bike_id ? (
<button
onClick={() => handleSave(bike.bike_id)}
className="bg-green-700 text-white px-3 py-1 rounded hover:bg-green-600"
>
Save
</button>
) : (
<button
onClick={() => setEditingId(bike.bike_id)}
className="border border-green-700 text-green-700 px-3 py-1 rounded hover:bg-green-50"
>
Edit
</button>
)}
<button
onClick={() => handleDelete(bike.bike_id)}
className="border border-red-700 text-red-700 px-3 py-1 rounded hover:bg-red-700 hover:text-white"
>
Delete
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)
}

Loading