Skip to content

Commit 8600f4e

Browse files
committed
crud working fine : frontend
1 parent 10f536c commit 8600f4e

File tree

44 files changed

+2573
-269
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+2573
-269
lines changed

frontend/package-lock.json

Lines changed: 39 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
},
1111
"dependencies": {
1212
"@tanstack/react-query": "^5.90.12",
13+
"@vercel/analytics": "^1.6.1",
1314
"class-variance-authority": "^0.7.1",
1415
"clsx": "^2.1.1",
1516
"lucide-react": "^0.561.0",

frontend/src/app/admin/layout.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export default function AdminLayout({ children }: { children: React.ReactNode }) {
2+
return (
3+
<div className="min-h-screen bg-gray-100">
4+
<header className="bg-black text-white px-6 py-4 font-bold">
5+
Sabhyatam Admin
6+
</header>
7+
<main className="p-6">{children}</main>
8+
</div>
9+
)
10+
}
Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
"use client"
2+
3+
import { useEffect, useState } from "react"
4+
import { useParams, useRouter } from "next/navigation"
5+
import {
6+
adminGetProduct,
7+
adminUpdateProduct,
8+
adminAddMedia,
9+
adminDeleteMedia,
10+
} from "@/lib/admin-api"
11+
import type { AdminProduct, ProductMedia } from "@/lib/types"
12+
13+
export default function AdminEditProductPage() {
14+
const params = useParams()
15+
const router = useRouter()
16+
17+
const id = params?.id as string | undefined
18+
19+
const [loading, setLoading] = useState(true)
20+
const [saving, setSaving] = useState(false)
21+
const [error, setError] = useState<string | null>(null)
22+
23+
const [product, setProduct] = useState<AdminProduct | null>(null)
24+
const [media, setMedia] = useState<ProductMedia[]>([])
25+
26+
const [mediaForm, setMediaForm] = useState({
27+
url: "",
28+
role: "gallery" as "hero" | "gallery",
29+
order: 1,
30+
})
31+
32+
// -----------------------------
33+
// Load product + media
34+
// -----------------------------
35+
useEffect(() => {
36+
if (!id) return
37+
load(id)
38+
}, [id])
39+
40+
async function load(productId: string) {
41+
try {
42+
setLoading(true)
43+
const res = await adminGetProduct(productId)
44+
setProduct(res.product)
45+
setMedia(res.media ?? [])
46+
} catch {
47+
setError("Failed to load product")
48+
} finally {
49+
setLoading(false)
50+
}
51+
}
52+
53+
// -----------------------------
54+
// Save product
55+
// -----------------------------
56+
async function saveProduct(e: React.FormEvent) {
57+
e.preventDefault()
58+
if (!id || !product) return
59+
60+
setSaving(true)
61+
setError(null)
62+
63+
try {
64+
await adminUpdateProduct(id, {
65+
title: product.title,
66+
slug: product.slug,
67+
category: product.category,
68+
description: product.description,
69+
price: Number(product.price),
70+
mrp: product.mrp ? Number(product.mrp) : undefined,
71+
in_stock: product.in_stock,
72+
published: product.published,
73+
})
74+
} catch (e: any) {
75+
setError(e.message || "Save failed")
76+
} finally {
77+
setSaving(false)
78+
}
79+
}
80+
81+
// -----------------------------
82+
// Add image
83+
// -----------------------------
84+
async function addImage() {
85+
if (!id || !mediaForm.url) return
86+
87+
const m = await adminAddMedia(id, {
88+
url: mediaForm.url,
89+
media_type: "image",
90+
meta: {
91+
role: mediaForm.role,
92+
order: mediaForm.order,
93+
},
94+
})
95+
96+
setMedia(xs => [...xs, m])
97+
setMediaForm({ url: "", role: "gallery", order: 1 })
98+
}
99+
100+
// -----------------------------
101+
// Delete image
102+
// -----------------------------
103+
async function removeImage(mediaId: string) {
104+
await adminDeleteMedia(mediaId)
105+
setMedia(xs => xs.filter(m => m.id !== mediaId))
106+
}
107+
108+
// -----------------------------
109+
// Render guards
110+
// -----------------------------
111+
if (!id) return <div>Invalid product</div>
112+
if (loading) return <div>Loading...</div>
113+
if (!product) return <div>Product not found</div>
114+
115+
return (
116+
<div className="max-w-4xl space-y-10">
117+
{/* ---------------- PRODUCT FORM ---------------- */}
118+
<form onSubmit={saveProduct} className="bg-white p-6 rounded shadow">
119+
<h1 className="text-xl font-bold mb-4">Edit Product</h1>
120+
121+
{error && <div className="text-red-600 mb-3">{error}</div>}
122+
123+
<div className="space-y-3">
124+
<input
125+
className="w-full border p-2"
126+
value={product.title}
127+
onChange={e =>
128+
setProduct({ ...product, title: e.target.value })
129+
}
130+
placeholder="Title"
131+
/>
132+
133+
<input
134+
className="w-full border p-2"
135+
value={product.slug}
136+
onChange={e =>
137+
setProduct({ ...product, slug: e.target.value })
138+
}
139+
placeholder="Slug"
140+
/>
141+
142+
<input
143+
className="w-full border p-2"
144+
value={product.category}
145+
onChange={e =>
146+
setProduct({ ...product, category: e.target.value })
147+
}
148+
placeholder="Category"
149+
/>
150+
151+
<textarea
152+
className="w-full border p-2"
153+
value={product.description ?? ""}
154+
onChange={e =>
155+
setProduct({ ...product, description: e.target.value })
156+
}
157+
placeholder="Description"
158+
/>
159+
160+
<div className="grid grid-cols-2 gap-4">
161+
<input
162+
type="number"
163+
className="border p-2"
164+
value={product.price}
165+
onChange={e =>
166+
setProduct({ ...product, price: Number(e.target.value) })
167+
}
168+
placeholder="Price"
169+
/>
170+
171+
<input
172+
type="number"
173+
className="border p-2"
174+
value={product.mrp ?? ""}
175+
onChange={e =>
176+
setProduct({
177+
...product,
178+
mrp: e.target.value
179+
? Number(e.target.value)
180+
: undefined,
181+
})
182+
}
183+
placeholder="MRP"
184+
/>
185+
</div>
186+
187+
<div className="flex gap-6">
188+
<label>
189+
<input
190+
type="checkbox"
191+
checked={product.in_stock}
192+
onChange={e =>
193+
setProduct({
194+
...product,
195+
in_stock: e.target.checked,
196+
})
197+
}
198+
/>{" "}
199+
In stock
200+
</label>
201+
202+
<label>
203+
<input
204+
type="checkbox"
205+
checked={product.published}
206+
onChange={e =>
207+
setProduct({
208+
...product,
209+
published: e.target.checked,
210+
})
211+
}
212+
/>{" "}
213+
Published
214+
</label>
215+
</div>
216+
217+
<button
218+
disabled={saving}
219+
className="bg-black text-white px-4 py-2"
220+
>
221+
{saving ? "Saving..." : "Save Product"}
222+
</button>
223+
</div>
224+
</form>
225+
226+
{/* ---------------- IMAGE MANAGEMENT ---------------- */}
227+
<section className="bg-white p-6 rounded shadow">
228+
<h2 className="text-lg font-bold mb-4">Product Images</h2>
229+
230+
<div className="flex gap-2 mb-4">
231+
<input
232+
className="flex-1 border p-2"
233+
placeholder="Image URL"
234+
value={mediaForm.url}
235+
onChange={e =>
236+
setMediaForm(f => ({ ...f, url: e.target.value }))
237+
}
238+
/>
239+
240+
<select
241+
className="border p-2"
242+
value={mediaForm.role}
243+
onChange={e =>
244+
setMediaForm(f => ({
245+
...f,
246+
role: e.target.value as "hero" | "gallery",
247+
}))
248+
}
249+
>
250+
<option value="hero">Hero</option>
251+
<option value="gallery">Gallery</option>
252+
</select>
253+
254+
<input
255+
type="number"
256+
className="w-20 border p-2"
257+
value={mediaForm.order}
258+
onChange={e =>
259+
setMediaForm(f => ({
260+
...f,
261+
order: Number(e.target.value),
262+
}))
263+
}
264+
/>
265+
266+
<button
267+
onClick={addImage}
268+
className="bg-black text-white px-3"
269+
>
270+
Add
271+
</button>
272+
</div>
273+
274+
<div className="grid grid-cols-4 gap-4">
275+
{media.map(m => (
276+
<div key={m.id} className="border p-2 relative">
277+
<img
278+
src={m.url}
279+
className="w-full h-32 object-cover"
280+
/>
281+
282+
<div className="text-xs mt-1">
283+
<div>{m.meta.role}</div>
284+
<div>Order: {m.meta.order}</div>
285+
</div>
286+
287+
<button
288+
onClick={() => removeImage(m.id)}
289+
className="absolute top-1 right-1 text-red-600 text-xs"
290+
>
291+
292+
</button>
293+
</div>
294+
))}
295+
</div>
296+
</section>
297+
</div>
298+
)
299+
}

0 commit comments

Comments
 (0)