functional account and item management
This commit is contained in:
parent
ed5afb1dec
commit
9e52f569f8
1
public/icons/account.svg
Normal file
1
public/icons/account.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M234-276q51-39 114-61.5T480-360q69 0 132 22.5T726-276q35-41 54.5-93T800-480q0-133-93.5-226.5T480-800q-133 0-226.5 93.5T160-480q0 59 19.5 111t54.5 93Zm246-164q-59 0-99.5-40.5T340-580q0-59 40.5-99.5T480-720q59 0 99.5 40.5T620-580q0 59-40.5 99.5T480-440Zm0 360q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q53 0 100-15.5t86-44.5q-39-29-86-44.5T480-280q-53 0-100 15.5T294-220q39 29 86 44.5T480-160Zm0-360q26 0 43-17t17-43q0-26-17-43t-43-17q-26 0-43 17t-17 43q0 26 17 43t43 17Zm0-60Zm0 360Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 751 B |
1
public/icons/cart.svg
Normal file
1
public/icons/cart.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M280-80q-33 0-56.5-23.5T200-160q0-33 23.5-56.5T280-240q33 0 56.5 23.5T360-160q0 33-23.5 56.5T280-80Zm400 0q-33 0-56.5-23.5T600-160q0-33 23.5-56.5T680-240q33 0 56.5 23.5T760-160q0 33-23.5 56.5T680-80ZM246-720l96 200h280l110-200H246Zm-38-80h590q23 0 35 20.5t1 41.5L692-482q-11 20-29.5 31T622-440H324l-44 80h480v80H280q-45 0-68-39.5t-2-78.5l54-98-144-304H40v-80h130l38 80Zm134 280h280-280Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 511 B |
1
public/icons/list_add.svg
Normal file
1
public/icons/list_add.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M680-40v-120H560v-80h120v-120h80v120h120v80H760v120h-80ZM200-200v-560 560Zm0 80q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v353q-18-11-38-18t-42-11v-324H200v560h280q0 21 3 41t10 39H200Zm120-160q17 0 28.5-11.5T360-320q0-17-11.5-28.5T320-360q-17 0-28.5 11.5T280-320q0 17 11.5 28.5T320-280Zm0-160q17 0 28.5-11.5T360-480q0-17-11.5-28.5T320-520q-17 0-28.5 11.5T280-480q0 17 11.5 28.5T320-440Zm0-160q17 0 28.5-11.5T360-640q0-17-11.5-28.5T320-680q-17 0-28.5 11.5T280-640q0 17 11.5 28.5T320-600Zm120 160h240v-80H440v80Zm0-160h240v-80H440v80Zm0 320h54q8-23 20-43t28-37H440v80Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 727 B |
@ -1,19 +1,13 @@
|
|||||||
// make api route to get all sellables
|
// make api route to get all sellables
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { auth } from "~/server/auth";
|
|
||||||
import { db } from "~/server/db";
|
import { db } from "~/server/db";
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
const session = await auth();
|
const sellables = await db.item.findMany({
|
||||||
|
select: {
|
||||||
if (!session) {
|
shop: { select: { label: true, id: true } },
|
||||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
item_name: true,
|
||||||
}
|
stock: true,
|
||||||
|
|
||||||
const sellables = await db.sellable.findMany({
|
|
||||||
include: {
|
|
||||||
shop: { select: { label: true } },
|
|
||||||
item: { select: { stock: true } },
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
// and cache no store header
|
// and cache no store header
|
||||||
|
|||||||
112
src/app/api/sellable/route.ts
Normal file
112
src/app/api/sellable/route.ts
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
// make api route to get all sellables
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { db } from "~/server/db";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
const sellables = await db.sellable.findMany({
|
||||||
|
include: {
|
||||||
|
shop: { select: { label: true } },
|
||||||
|
item: { select: { stock: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// and cache no store header
|
||||||
|
|
||||||
|
return NextResponse.json(sellables, {
|
||||||
|
headers: { "Cache-Control": "no-store" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
type Sellable = {
|
||||||
|
shopId: number;
|
||||||
|
itemId: string;
|
||||||
|
itemName: string;
|
||||||
|
price: number;
|
||||||
|
amount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
const { shopId, itemId, price, amount } = (await request.json()) as Sellable;
|
||||||
|
|
||||||
|
const sellable = await db.sellable.create({
|
||||||
|
data: {
|
||||||
|
shopId,
|
||||||
|
item_name: itemId,
|
||||||
|
price,
|
||||||
|
amount,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json(sellable, {
|
||||||
|
headers: { "Cache-Control": "no-store" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function PATCH(request: Request) {
|
||||||
|
const { shopId, itemId, price, amount } = (await request.json()) as Sellable;
|
||||||
|
|
||||||
|
if (!shopId || !itemId) {
|
||||||
|
return NextResponse.json({ error: "Invalid payload" }, { status: 400 });
|
||||||
|
}
|
||||||
|
const item = await db.sellable.findFirst({
|
||||||
|
where: {
|
||||||
|
shopId,
|
||||||
|
item_name: itemId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!item) {
|
||||||
|
return NextResponse.json({ error: "Item not found" }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = await db.sellable.update({
|
||||||
|
where: {
|
||||||
|
id: item.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
price,
|
||||||
|
amount,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json(updated, {
|
||||||
|
headers: { "Cache-Control": "no-store" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function DELETE(request: Request) {
|
||||||
|
const { shopId, itemId } = (await request.json()) as Sellable;
|
||||||
|
|
||||||
|
if (!shopId || !itemId) {
|
||||||
|
return NextResponse.json({ error: "Invalid payload" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = await db.sellable.findFirst({
|
||||||
|
where: {
|
||||||
|
shopId,
|
||||||
|
item_name: itemId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!item) {
|
||||||
|
return NextResponse.json({ error: "Item not found" }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.sellable.delete({
|
||||||
|
where: {
|
||||||
|
id: item.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: true },
|
||||||
|
{
|
||||||
|
headers: { "Cache-Control": "no-store" },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -6,6 +6,8 @@ import Items from "~/components/sellable_items";
|
|||||||
import Search from "~/components/search";
|
import Search from "~/components/search";
|
||||||
import CartButton from "~/components/cart";
|
import CartButton from "~/components/cart";
|
||||||
import type { Session } from "next-auth";
|
import type { Session } from "next-auth";
|
||||||
|
import AccountButton from "./account";
|
||||||
|
import SellableItemsButton from "./new_items";
|
||||||
|
|
||||||
type UserResponse = {
|
type UserResponse = {
|
||||||
id: string;
|
id: string;
|
||||||
@ -19,6 +21,20 @@ type UserResponse = {
|
|||||||
balance: number;
|
balance: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type SellableResponse = {
|
||||||
|
id: string;
|
||||||
|
item_name: string;
|
||||||
|
amount: number;
|
||||||
|
price: number;
|
||||||
|
enabled: boolean;
|
||||||
|
shop: {
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
item: {
|
||||||
|
stock: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
session: Session | null;
|
session: Session | null;
|
||||||
};
|
};
|
||||||
@ -26,6 +42,9 @@ type Props = {
|
|||||||
export default function HomeClient({ session }: Props) {
|
export default function HomeClient({ session }: Props) {
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
const [userData, setUserData] = useState<UserResponse | null>(null);
|
const [userData, setUserData] = useState<UserResponse | null>(null);
|
||||||
|
const [sellableData, setSellableData] = useState<SellableResponse | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
// Fetch /api/user once and store globally here
|
// Fetch /api/user once and store globally here
|
||||||
@ -44,8 +63,27 @@ export default function HomeClient({ session }: Props) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!session) return;
|
||||||
void loadUser();
|
void loadUser();
|
||||||
}, [loadUser]);
|
}, [loadUser, session]);
|
||||||
|
|
||||||
|
const loadSellable = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const res = await fetch("/api/sellable");
|
||||||
|
if (!res.ok) throw new Error("Failed to fetch sellable");
|
||||||
|
const data = (await res.json()) as SellableResponse;
|
||||||
|
setSellableData(data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void loadSellable();
|
||||||
|
}, [loadSellable]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex min-h-screen flex-col items-center justify-center bg-linear-to-b from-[#2e026d] to-[#7f3b3b] text-white">
|
<main className="flex min-h-screen flex-col items-center justify-center bg-linear-to-b from-[#2e026d] to-[#7f3b3b] text-white">
|
||||||
@ -55,11 +93,21 @@ export default function HomeClient({ session }: Props) {
|
|||||||
${userData.balance ?? 0}
|
${userData.balance ?? 0}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{session?.user && (
|
||||||
|
<>
|
||||||
<CartButton
|
<CartButton
|
||||||
userData={userData}
|
userData={userData}
|
||||||
reloadUser={loadUser}
|
reloadUser={loadUser}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
/>
|
/>
|
||||||
|
<AccountButton loading={loading} />
|
||||||
|
<SellableItemsButton
|
||||||
|
loading={loading}
|
||||||
|
reloadSellable={loadSellable}
|
||||||
|
reloadUser={loadUser}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<Search query={query} setQuery={setQuery} />
|
<Search query={query} setQuery={setQuery} />
|
||||||
|
|
||||||
@ -84,8 +132,13 @@ export default function HomeClient({ session }: Props) {
|
|||||||
<h1 className="text-5xl font-extrabold tracking-tight">
|
<h1 className="text-5xl font-extrabold tracking-tight">
|
||||||
Suchodupin <span className="text-[hsl(280,100%,70%)]">MC</span> Shop
|
Suchodupin <span className="text-[hsl(280,100%,70%)]">MC</span> Shop
|
||||||
</h1>
|
</h1>
|
||||||
|
{sellableData && (
|
||||||
<Items query={query} reloadUser={loadUser} />
|
<Items
|
||||||
|
query={query}
|
||||||
|
sellableData={sellableData}
|
||||||
|
reloadUser={loadUser}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
264
src/components/account.tsx
Normal file
264
src/components/account.tsx
Normal file
@ -0,0 +1,264 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
type Shop = {
|
||||||
|
id: number;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type UserData = {
|
||||||
|
adresses: string[];
|
||||||
|
shops: Shop[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
loading: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AccountButton({ loading }: Props) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [userData, setUserData] = useState<UserData>({
|
||||||
|
adresses: [],
|
||||||
|
shops: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const [newAddress, setNewAddress] = useState("");
|
||||||
|
const [shopId, setShopId] = useState<number | "">("");
|
||||||
|
const [shopLabel, setShopLabel] = useState("");
|
||||||
|
|
||||||
|
/* ─────────────── LOAD USER ON OPEN ─────────────── */
|
||||||
|
|
||||||
|
const loadUser = async (): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/user");
|
||||||
|
if (!res.ok) {
|
||||||
|
console.error("Failed to load user data");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
setUserData({
|
||||||
|
adresses: data.adresses ?? [],
|
||||||
|
shops: data.shops ?? [],
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error loading user data", err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openModal = async () => {
|
||||||
|
const ok = await loadUser();
|
||||||
|
if (ok) setIsOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ─────────────── SAVE ─────────────── */
|
||||||
|
|
||||||
|
const saveAll = async (next: UserData): Promise<Response | null> => {
|
||||||
|
try {
|
||||||
|
return await fetch("/api/user", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(next),
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Save failed", err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ─────────────── ADD / REMOVE ADDRESSES ─────────────── */
|
||||||
|
|
||||||
|
const addAddress = async () => {
|
||||||
|
const value = newAddress.trim();
|
||||||
|
if (!value) return;
|
||||||
|
|
||||||
|
const next = {
|
||||||
|
...userData,
|
||||||
|
adresses: [...userData.adresses, value],
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await saveAll(next);
|
||||||
|
if (!res || !res.ok) {
|
||||||
|
console.error("Failed to add address");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setUserData(next);
|
||||||
|
setNewAddress("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeAddress = async (adress: string) => {
|
||||||
|
const next = {
|
||||||
|
...userData,
|
||||||
|
adresses: userData.adresses.filter((a) => a !== adress),
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await saveAll(next);
|
||||||
|
if (!res || !res.ok) {
|
||||||
|
console.error("Failed to remove address");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setUserData(next);
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ─────────────── ADD / REMOVE SHOPS ─────────────── */
|
||||||
|
|
||||||
|
const addShop = async () => {
|
||||||
|
if (shopId === "" || !Number.isInteger(shopId)) return;
|
||||||
|
if (!shopLabel.trim()) return;
|
||||||
|
|
||||||
|
const next = {
|
||||||
|
...userData,
|
||||||
|
shops: [...userData.shops, { id: shopId, label: shopLabel.trim() }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await saveAll(next);
|
||||||
|
if (!res || !res.ok) {
|
||||||
|
console.error("Failed to add shop");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setUserData(next);
|
||||||
|
setShopId("");
|
||||||
|
setShopLabel("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeShop = async (id: number) => {
|
||||||
|
const next = {
|
||||||
|
...userData,
|
||||||
|
shops: userData.shops.filter((s) => s.id !== id),
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await saveAll(next);
|
||||||
|
if (!res || !res.ok) {
|
||||||
|
console.error("Failed to remove shop");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setUserData(next);
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ─────────────── UI ─────────────── */
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* BUTTON */}
|
||||||
|
<button
|
||||||
|
className="relative rounded-full bg-white/10 px-4 py-2 font-semibold hover:bg-white/20"
|
||||||
|
disabled={loading}
|
||||||
|
onClick={openModal}
|
||||||
|
>
|
||||||
|
<img src="/icons/account.svg" alt="User Icon" className="h-6 w-6" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* MODAL */}
|
||||||
|
{isOpen && (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
|
||||||
|
onClick={() => setIsOpen(false)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="relative z-10 w-full max-w-lg rounded-xl bg-neutral-900 p-6 text-white shadow-xl">
|
||||||
|
<h2 className="mb-6 text-xl font-bold">Account Settings</h2>
|
||||||
|
|
||||||
|
{/* ───────── ADDRESSES ───────── */}
|
||||||
|
<section className="mb-6">
|
||||||
|
<h3 className="mb-2 text-lg font-semibold">Addresses</h3>
|
||||||
|
|
||||||
|
<ul className="mb-3 space-y-2">
|
||||||
|
{userData.adresses.map((adress) => (
|
||||||
|
<li
|
||||||
|
key={adress}
|
||||||
|
className="flex items-center justify-between rounded bg-white/5 px-3 py-2"
|
||||||
|
>
|
||||||
|
<span className="text-sm break-all">{adress}</span>
|
||||||
|
<button
|
||||||
|
onClick={() => removeAddress(adress)}
|
||||||
|
className="rounded bg-red-500 px-2 py-1 text-xs font-semibold hover:bg-red-600"
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
value={newAddress}
|
||||||
|
onChange={(e) => setNewAddress(e.target.value)}
|
||||||
|
placeholder="PKP Centralny: Poczta - zaremate"
|
||||||
|
className="flex-1 rounded bg-white/10 px-3 py-2 text-sm"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={addAddress}
|
||||||
|
className="rounded bg-green-500 px-3 py-2 text-sm font-bold hover:bg-green-600"
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* ───────── SHOPS ───────── */}
|
||||||
|
<section>
|
||||||
|
<h3 className="mb-2 text-lg font-semibold">Shops</h3>
|
||||||
|
|
||||||
|
<ul className="mb-3 space-y-2">
|
||||||
|
{userData.shops.map((shop) => (
|
||||||
|
<li
|
||||||
|
key={shop.id}
|
||||||
|
className="flex items-center justify-between rounded bg-white/5 px-3 py-2"
|
||||||
|
>
|
||||||
|
<span className="text-sm">
|
||||||
|
#{shop.id} — {shop.label}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => removeShop(shop.id)}
|
||||||
|
className="rounded bg-red-500 px-2 py-1 text-xs font-semibold hover:bg-red-600"
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step={1}
|
||||||
|
value={shopId}
|
||||||
|
onChange={(e) =>
|
||||||
|
setShopId(
|
||||||
|
e.target.value === "" ? "" : Number(e.target.value),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
placeholder="Shop ID (e.g. 12)"
|
||||||
|
className="rounded bg-white/10 px-3 py-2 text-sm"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
value={shopLabel}
|
||||||
|
onChange={(e) => setShopLabel(e.target.value)}
|
||||||
|
placeholder="Label"
|
||||||
|
className="rounded bg-white/10 px-3 py-2 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={addShop}
|
||||||
|
className="mt-3 w-full rounded bg-green-500 py-2 font-bold hover:bg-green-600"
|
||||||
|
>
|
||||||
|
Add Shop
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { IMAGES_MANIFEST } from "next/dist/shared/lib/constants";
|
||||||
import { useEffect, useState, useCallback } from "react";
|
import { useEffect, useState, useCallback } from "react";
|
||||||
|
|
||||||
type UserResponse = {
|
type UserResponse = {
|
||||||
@ -80,7 +81,7 @@ export default function CartButton({
|
|||||||
if (!userData) return;
|
if (!userData) return;
|
||||||
|
|
||||||
const fetchItems = async () => {
|
const fetchItems = async () => {
|
||||||
const res = await fetch("/api/items");
|
const res = await fetch("/api/sellable");
|
||||||
const items = (await res.json()) as ApiItem[];
|
const items = (await res.json()) as ApiItem[];
|
||||||
const itemMap = new Map(items.map((i) => [i.id, i]));
|
const itemMap = new Map(items.map((i) => [i.id, i]));
|
||||||
|
|
||||||
@ -196,7 +197,7 @@ export default function CartButton({
|
|||||||
disabled={loading}
|
disabled={loading}
|
||||||
onClick={() => setIsOpen(true)}
|
onClick={() => setIsOpen(true)}
|
||||||
>
|
>
|
||||||
🛒 Cart
|
<img src="/icons/cart.svg" alt="Cart Icon" className="h-6 w-6" />
|
||||||
{totalQuantity > 0 && (
|
{totalQuantity > 0 && (
|
||||||
<span className="absolute -top-1 -right-1 flex h-5 min-w-5 items-center justify-center rounded-full bg-red-500 px-1 text-xs font-bold text-white">
|
<span className="absolute -top-1 -right-1 flex h-5 min-w-5 items-center justify-center rounded-full bg-red-500 px-1 text-xs font-bold text-white">
|
||||||
{totalQuantity}
|
{totalQuantity}
|
||||||
|
|||||||
421
src/components/new_items.tsx
Normal file
421
src/components/new_items.tsx
Normal file
@ -0,0 +1,421 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
type Shop = {
|
||||||
|
id: number;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type UserData = {
|
||||||
|
shops: Shop[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type ItemFromApi = {
|
||||||
|
item_name: string;
|
||||||
|
stock: number;
|
||||||
|
shop: Shop;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SellableFromApi = {
|
||||||
|
shopId: number;
|
||||||
|
item_name: string;
|
||||||
|
price: number;
|
||||||
|
amount: number;
|
||||||
|
shop: {
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
loading: boolean;
|
||||||
|
reloadSellable: () => Promise<void>;
|
||||||
|
reloadUser: () => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SellableItemsButton({
|
||||||
|
loading,
|
||||||
|
reloadSellable,
|
||||||
|
reloadUser,
|
||||||
|
}: Props) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
const [items, setItems] = useState<ItemFromApi[]>([]);
|
||||||
|
const [sellables, setSellables] = useState<SellableFromApi[]>([]);
|
||||||
|
const [userShops, setUserShops] = useState<Shop[]>([]);
|
||||||
|
|
||||||
|
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
|
||||||
|
const [price, setPrice] = useState<number | "">("");
|
||||||
|
const [amount, setAmount] = useState<number | "">("");
|
||||||
|
|
||||||
|
const [editingKey, setEditingKey] = useState<string | null>(null);
|
||||||
|
const [editPrice, setEditPrice] = useState<number | "">("");
|
||||||
|
const [editAmount, setEditAmount] = useState<number | "">("");
|
||||||
|
|
||||||
|
// Format price as 0.00$
|
||||||
|
const formatPrice = (p: number) => `${p.toFixed(2)}$`;
|
||||||
|
const sellableKey = (s: SellableFromApi) => `${s.shopId}:${s.item_name}`;
|
||||||
|
|
||||||
|
/* ─────────────── LOAD ALL DATA ─────────────── */
|
||||||
|
const loadAll = async (): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
const [userRes, itemsRes, sellablesRes] = await Promise.all([
|
||||||
|
fetch("/api/user", { cache: "no-store" }),
|
||||||
|
fetch("/api/items", { cache: "no-store" }),
|
||||||
|
fetch("/api/sellable", { cache: "no-store" }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!userRes.ok || !itemsRes.ok || !sellablesRes.ok) {
|
||||||
|
console.error("Failed to load data");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userData: UserData = (await userRes.json()) as UserData;
|
||||||
|
const itemsData = (await itemsRes.json()) as ItemFromApi[];
|
||||||
|
const sellablesData = (await sellablesRes.json()) as SellableFromApi[];
|
||||||
|
|
||||||
|
setUserShops(userData.shops ?? []);
|
||||||
|
setItems(itemsData);
|
||||||
|
setSellables(sellablesData);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error loading data", err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openModal = async () => {
|
||||||
|
const ok = await loadAll();
|
||||||
|
if (ok) setIsOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ─────────────── ADD SELLABLE ─────────────── */
|
||||||
|
const addSellable = async () => {
|
||||||
|
if (selectedIndex === null || price === "" || amount === "") return;
|
||||||
|
|
||||||
|
const item = items[selectedIndex];
|
||||||
|
|
||||||
|
if (!item) return;
|
||||||
|
|
||||||
|
const res = await fetch("/api/sellable", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
shopId: item.shop.id,
|
||||||
|
itemId: item.item_name,
|
||||||
|
price,
|
||||||
|
amount,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
console.error("Failed to add sellable");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update state locally so item disappears immediately
|
||||||
|
setSellables((prev) => [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
shopId: item.shop.id,
|
||||||
|
item_name: item.item_name,
|
||||||
|
price,
|
||||||
|
amount,
|
||||||
|
shop: { label: item.shop.label },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
setSelectedIndex(null);
|
||||||
|
setPrice("");
|
||||||
|
setAmount("");
|
||||||
|
|
||||||
|
void reloadSellable();
|
||||||
|
void reloadUser();
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ─────────────── REMOVE SELLABLE ─────────────── */
|
||||||
|
const removeSellable = async (shopId: number, itemId: string) => {
|
||||||
|
const res = await fetch("/api/sellable", {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ shopId, itemId }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
console.error("Failed to remove sellable");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove locally
|
||||||
|
setSellables((prev) =>
|
||||||
|
prev.filter((s) => !(s.shopId === shopId && s.item_name === itemId)),
|
||||||
|
);
|
||||||
|
|
||||||
|
void reloadSellable();
|
||||||
|
void reloadUser();
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ─────────────── EDIT SELLABLE ─────────────── */
|
||||||
|
const saveEdit = async (shopId: number, itemId: string) => {
|
||||||
|
if (editPrice === "" || editAmount === "") return;
|
||||||
|
|
||||||
|
const res = await fetch("/api/sellable", {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
shopId,
|
||||||
|
itemId,
|
||||||
|
price: editPrice,
|
||||||
|
amount: editAmount,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
console.error("Failed to update sellable");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update locally
|
||||||
|
setSellables((prev) =>
|
||||||
|
prev.map((s) =>
|
||||||
|
s.shopId === shopId && s.item_name === itemId
|
||||||
|
? { ...s, price: editPrice, amount: editAmount }
|
||||||
|
: s,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
setEditingKey(null);
|
||||||
|
void reloadSellable();
|
||||||
|
void reloadUser();
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ─────────────── FILTERING ─────────────── */
|
||||||
|
const userShopIds = new Set(userShops.map((s) => s.id));
|
||||||
|
|
||||||
|
// Items available to add (user's shops, not already sold)
|
||||||
|
const availableItems = items
|
||||||
|
.filter((item) => userShopIds.has(item.shop.id))
|
||||||
|
.filter(
|
||||||
|
(item) =>
|
||||||
|
!sellables.some(
|
||||||
|
(s) => s.shopId === item.shop.id && s.item_name === item.item_name,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Sellables grouped by user's shops
|
||||||
|
const sellablesByShop = sellables.reduce<Record<number, SellableFromApi[]>>(
|
||||||
|
(acc, s) => {
|
||||||
|
if (!userShopIds.has(s.shopId)) return acc;
|
||||||
|
acc[s.shopId] ??= [];
|
||||||
|
acc[s.shopId].push(s);
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
/* ─────────────── UI ─────────────── */
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* BUTTON */}
|
||||||
|
<button
|
||||||
|
className="relative rounded-full bg-white/10 px-4 py-2 font-semibold hover:bg-white/20"
|
||||||
|
disabled={loading}
|
||||||
|
onClick={openModal}
|
||||||
|
>
|
||||||
|
<img src="/icons/list_add.svg" alt="Shop Icon" className="h-6 w-6" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* MODAL */}
|
||||||
|
{isOpen && (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
|
||||||
|
onClick={() => setIsOpen(false)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="relative z-10 w-full max-w-2xl rounded-xl bg-neutral-900 p-6 text-white shadow-xl">
|
||||||
|
<h2 className="mb-6 text-xl font-bold">Store Items</h2>
|
||||||
|
|
||||||
|
{/* ───────── CURRENTLY SOLD ───────── */}
|
||||||
|
<section className="mb-8">
|
||||||
|
<h3 className="mb-3 text-lg font-semibold">
|
||||||
|
Currently Sold (Your Shops)
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{Object.keys(sellablesByShop).length === 0 && (
|
||||||
|
<p className="text-sm text-neutral-400">
|
||||||
|
No items are currently sold.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{Object.entries(sellablesByShop).map(([shopId, items]) => (
|
||||||
|
<div key={shopId} className="rounded-lg bg-white/5 p-3">
|
||||||
|
<h4 className="mb-2 font-semibold">
|
||||||
|
{items[0]?.shop.label ?? `Shop ${shopId}`}
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<ul className="space-y-1 text-sm">
|
||||||
|
{items.map((s) => {
|
||||||
|
const key = sellableKey(s);
|
||||||
|
const isEditing = editingKey === key;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={key}
|
||||||
|
className="flex items-center justify-between gap-2 rounded bg-black/20 px-2 py-1"
|
||||||
|
>
|
||||||
|
<span className="flex-1">{s.item_name}</span>
|
||||||
|
|
||||||
|
{isEditing ? (
|
||||||
|
<>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step={0.01}
|
||||||
|
value={editPrice}
|
||||||
|
onChange={(e) =>
|
||||||
|
setEditPrice(
|
||||||
|
e.target.value === ""
|
||||||
|
? ""
|
||||||
|
: Number(e.target.value),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="w-20 rounded bg-white/10 px-2 py-1 text-xs"
|
||||||
|
placeholder="Price (0.00$)"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={editAmount}
|
||||||
|
min={1}
|
||||||
|
onChange={(e) =>
|
||||||
|
setEditAmount(
|
||||||
|
e.target.value === ""
|
||||||
|
? ""
|
||||||
|
: Number(e.target.value),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="w-20 rounded bg-white/10 px-2 py-1 text-xs"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
saveEdit(s.shopId, s.item_name)
|
||||||
|
}
|
||||||
|
className="text-xs font-bold text-green-400"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setEditingKey(null)}
|
||||||
|
className="text-xs text-neutral-400"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span className="text-xs text-neutral-400">
|
||||||
|
{s.amount} × {formatPrice(s.price)}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setEditingKey(key);
|
||||||
|
setEditPrice(s.price);
|
||||||
|
setEditAmount(s.amount);
|
||||||
|
}}
|
||||||
|
className="text-xs text-blue-400"
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
removeSellable(s.shopId, s.item_name)
|
||||||
|
}
|
||||||
|
className="text-xs text-red-400"
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* ───────── ADD NEW ───────── */}
|
||||||
|
<section>
|
||||||
|
<h3 className="mb-3 text-lg font-semibold">Add Item to Store</h3>
|
||||||
|
|
||||||
|
<ul className="mb-4 max-h-56 space-y-2 overflow-y-auto">
|
||||||
|
{availableItems.map((item, index) => (
|
||||||
|
<li
|
||||||
|
key={`${item.shop.id}-${item.item_name}`}
|
||||||
|
onClick={() => setSelectedIndex(index)}
|
||||||
|
className={`cursor-pointer rounded px-3 py-2 ${
|
||||||
|
selectedIndex === index
|
||||||
|
? "bg-green-500/20 ring-1 ring-green-500"
|
||||||
|
: "bg-white/5 hover:bg-white/10"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span>{item.item_name}</span>
|
||||||
|
<span className="text-neutral-400">
|
||||||
|
{item.shop.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-neutral-400">
|
||||||
|
Stock: {item.stock}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step={0.01}
|
||||||
|
min={0}
|
||||||
|
value={price}
|
||||||
|
onChange={(e) =>
|
||||||
|
setPrice(
|
||||||
|
e.target.value === "" ? "" : Number(e.target.value),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
placeholder="Price (0.00$)"
|
||||||
|
className="rounded bg-white/10 px-3 py-2 text-sm"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
value={amount}
|
||||||
|
onChange={(e) =>
|
||||||
|
setAmount(
|
||||||
|
e.target.value === "" ? "" : Number(e.target.value),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
placeholder="Amount"
|
||||||
|
className="rounded bg-white/10 px-3 py-2 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={addSellable}
|
||||||
|
disabled={selectedIndex === null}
|
||||||
|
className="mt-4 w-full rounded bg-green-500 py-3 font-bold text-black hover:bg-green-600 disabled:bg-gray-400"
|
||||||
|
>
|
||||||
|
Add to Store
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState, useCallback } from "react";
|
import { useState, useCallback, useEffect } from "react";
|
||||||
|
|
||||||
type ApiItem = {
|
type ApiItem = {
|
||||||
id: string;
|
id: string;
|
||||||
@ -32,11 +32,12 @@ const getImage = (name: string) => {
|
|||||||
|
|
||||||
type ItemsProps = {
|
type ItemsProps = {
|
||||||
query: string;
|
query: string;
|
||||||
|
sellableData: ApiItem[];
|
||||||
reloadUser: () => Promise<void>;
|
reloadUser: () => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Items = ({ query, reloadUser }: ItemsProps) => {
|
const Items = ({ query, sellableData, reloadUser }: ItemsProps) => {
|
||||||
const [items, setItems] = useState<ApiItem[]>([]);
|
const items = sellableData || []; // <-- fallback to empty array
|
||||||
const [dragging, setDragging] = useState<{
|
const [dragging, setDragging] = useState<{
|
||||||
itemId: string;
|
itemId: string;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
@ -46,22 +47,6 @@ const Items = ({ query, reloadUser }: ItemsProps) => {
|
|||||||
const [sliderActive, setSliderActive] = useState<string | null>(null);
|
const [sliderActive, setSliderActive] = useState<string | null>(null);
|
||||||
const [holdTimeout, setHoldTimeout] = useState<NodeJS.Timeout | null>(null);
|
const [holdTimeout, setHoldTimeout] = useState<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
const loadItems = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const res = await fetch("/api/items", { cache: "no-store" });
|
|
||||||
const data = (await res.json()) as ApiItem[];
|
|
||||||
setItems(data.filter((item) => item.enabled));
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Failed to load items", err);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
void loadItems();
|
|
||||||
const interval = setInterval(() => void loadItems(), 60_000);
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, [loadItems]);
|
|
||||||
|
|
||||||
const buyItem = useCallback(
|
const buyItem = useCallback(
|
||||||
async (id: string, quantity: number) => {
|
async (id: string, quantity: number) => {
|
||||||
try {
|
try {
|
||||||
@ -72,15 +57,14 @@ const Items = ({ query, reloadUser }: ItemsProps) => {
|
|||||||
});
|
});
|
||||||
if (!res.ok) throw new Error("Buy failed");
|
if (!res.ok) throw new Error("Buy failed");
|
||||||
|
|
||||||
await reloadUser(); // all components now update
|
await reloadUser();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[reloadUser], // <- include reloadUser as dependency
|
[reloadUser],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Handle hold to activate slider
|
|
||||||
const handleMouseDown = (e: React.MouseEvent, item: ApiItem) => {
|
const handleMouseDown = (e: React.MouseEvent, item: ApiItem) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
@ -94,7 +78,7 @@ const Items = ({ query, reloadUser }: ItemsProps) => {
|
|||||||
max: item.item.stock / item.amount,
|
max: item.item.stock / item.amount,
|
||||||
rect: button,
|
rect: button,
|
||||||
});
|
});
|
||||||
}, 300); // 1 second threshold
|
}, 300);
|
||||||
|
|
||||||
setHoldTimeout(timeout);
|
setHoldTimeout(timeout);
|
||||||
};
|
};
|
||||||
@ -108,7 +92,7 @@ const Items = ({ query, reloadUser }: ItemsProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!sliderActive) {
|
if (!sliderActive) {
|
||||||
void buyItem(item.id, 1); // quick click
|
void buyItem(item.id, 1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -156,8 +140,6 @@ const Items = ({ query, reloadUser }: ItemsProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
{/* Blur and darken overlay */}
|
|
||||||
|
|
||||||
<div className="relative z-10 grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4">
|
<div className="relative z-10 grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||||
{filteredItems.map((item) => (
|
{filteredItems.map((item) => (
|
||||||
<div
|
<div
|
||||||
@ -191,7 +173,6 @@ const Items = ({ query, reloadUser }: ItemsProps) => {
|
|||||||
{item.price}$/{item.amount}
|
{item.price}$/{item.amount}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{/* Slider on button */}
|
|
||||||
{sliderActive === item.id && dragging && (
|
{sliderActive === item.id && dragging && (
|
||||||
<div className="pointer-events-none absolute inset-0 z-30 flex items-center justify-center">
|
<div className="pointer-events-none absolute inset-0 z-30 flex items-center justify-center">
|
||||||
<div className="relative h-full w-full rounded">
|
<div className="relative h-full w-full rounded">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user