-
Notifications
You must be signed in to change notification settings - Fork 5
Admin page #21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Admin page #21
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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> | ||
| ) | ||
| } | ||
|
|
| 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() | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| 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
|
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| 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
|
||||||||||||||||||||||||||||||||||
| 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
AI
Feb 17, 2026
There was a problem hiding this comment.
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)
| 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
AI
Feb 17, 2026
There was a problem hiding this comment.
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).
| 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
AI
Feb 17, 2026
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.