getting lost files back and making some changes p1

This commit is contained in:
2026-01-14 20:15:07 +01:00
parent 26d140f910
commit bcc5164abb
30609 changed files with 18693 additions and 77811 deletions
View File
+21
View File
@@ -0,0 +1,21 @@
// make api route to get all sellables
import { NextResponse } from "next/server";
import { auth } from "~/server/auth";
import { db } from "~/server/db";
export async function GET() {
const session = await auth();
if (!session) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const sellables = await db.sellable.findMany({
include: {
shop: { select: { label: true } },
item: { select: { stock: true } },
},
});
return NextResponse.json(sellables);
}
+175
View File
@@ -0,0 +1,175 @@
// make api route to get user data if user is logged in (nextauth)
import { NextResponse } from "next/server";
import { auth } from "~/server/auth";
import type { Session } from "next-auth";
import { db } from "~/server/db";
export async function PATCH(request: Request) {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { adress } = (await request.json()) as { adress: string };
if (!adress || typeof adress !== "string") {
return NextResponse.json({ error: "Invalid adress" }, { status: 400 });
}
const result = await db.adress.create({
data: {
adress,
userId: session.user.id,
},
});
return NextResponse.json(result, { status: 201 });
}
export async function GET() {
const session: Session | null = await auth();
if (!session?.user?.id) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const user = await db.user.findUnique({
where: { id: session.user.id },
select: {
id: true,
name: true,
adresses: {
select: { adress: true },
},
shops: {
select: { id: true, label: true, sellables: true },
},
},
});
if (!user) {
return NextResponse.json({ error: "User not found" }, { status: 404 });
}
return NextResponse.json({
...user,
adresses: user.adresses.map((a) => a.adress),
});
}
type ShopInput = {
id: number;
label: string;
};
export async function POST(request: Request) {
const session: Session | null = await auth();
if (!session?.user?.id) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const body = (await request.json()) as {
adresses: string[];
shops: ShopInput[];
};
if (!Array.isArray(body.adresses) || !Array.isArray(body.shops)) {
return NextResponse.json(
{ error: "Invalid input format" },
{ status: 400 },
);
}
/* ─────────────── ADDRESSES ─────────────── */
const currentAdresses = await db.adress.findMany({
where: { userId: session.user.id },
select: { id: true, adress: true },
});
const currentAdressSet = new Set(currentAdresses.map((a) => a.adress));
const incomingAdressSet = new Set(body.adresses);
// Add only new addresses
const adressesToAdd = body.adresses.filter((a) => !currentAdressSet.has(a));
// Remove only missing addresses
const adressesToDelete = currentAdresses
.filter((a) => !incomingAdressSet.has(a.adress))
.map((a) => a.id);
if (adressesToAdd.length > 0) {
await db.adress.createMany({
data: adressesToAdd.map((adress) => ({
adress,
userId: session.user.id,
})),
});
}
if (adressesToDelete.length > 0) {
await db.adress.deleteMany({
where: { id: { in: adressesToDelete } },
});
}
/* ─────────────── SHOPS ─────────────── */
const currentShops = await db.shop.findMany({
where: { userId: session.user.id },
select: { id: true, label: true },
});
const incomingShopMap = new Map(body.shops.map((s) => [s.id, s.label]));
const currentShopIds = new Set(currentShops.map((s) => s.id));
const incomingShopIds = new Set(body.shops.map((s) => s.id));
if (!Array.from(incomingShopIds).every((id) => Number.isInteger(id))) {
throw new Error("Invalid shop id");
}
// Create new shops
const shopsToCreate = body.shops.filter((s) => !currentShopIds.has(s.id));
if (shopsToCreate.length > 0) {
await db.shop.createMany({
data: shopsToCreate.map((s) => ({
id: s.id,
label: s.label,
userId: session.user.id,
})),
});
}
// Update labels ONLY if changed
const shopsToUpdate = currentShops.filter(
(s) => incomingShopMap.has(s.id) && incomingShopMap.get(s.id) !== s.label,
);
await Promise.all(
shopsToUpdate.map((s) =>
db.shop.update({
where: { id: s.id },
data: { label: incomingShopMap.get(s.id)! },
}),
),
);
// Delete removed shops (Sellables cascade)
const shopsToDelete = currentShops
.filter((s) => !incomingShopIds.has(s.id))
.map((s) => s.id);
if (shopsToDelete.length > 0) {
await db.shop.deleteMany({
where: { id: { in: shopsToDelete } },
});
}
/* ─────────────── RESPONSE ─────────────── */
return NextResponse.json({ success: true });
}
+204
View File
@@ -0,0 +1,204 @@
import { db } from "~/server/db";
import { auth } from "~/server/auth";
import { redirect } from "next/navigation";
import { revalidatePath } from "next/cache";
export default async function ItemsPage() {
const session = await auth();
if (!session?.user?.id) {
redirect("/sign-in");
}
const userId = session.user.id;
// fetch all items for shops owned by the user
const items = await db.sellable.findMany({
where: { shop: { userId: userId } },
include: { shop: true },
});
// extract unique shops for dropdown
const uniqueShops = await db.shop.findMany({
where: { userId: userId },
});
// --- Server actions ---
async function addItem(data: FormData) {
"use server";
const name = data.get("item_name") as string;
const price = parseFloat(data.get("price") as string);
const amount = parseInt(data.get("amount") as string);
const shopId = parseInt(data.get("shopId") as string);
if (!name || isNaN(price) || isNaN(amount) || isNaN(shopId)) return;
await db.sellable.create({
data: { item_name: name, price, amount, shopId },
});
revalidatePath("/items");
}
async function updateItem(data: FormData) {
"use server";
const id = data.get("id") as string;
const name = data.get("item_name") as string;
const price = parseFloat(data.get("price") as string);
const amount = parseInt(data.get("amount") as string);
if (!id || !name || isNaN(price) || isNaN(amount)) return;
await db.sellable.update({
where: { id },
data: { item_name: name, price, amount },
});
revalidatePath("/items");
}
async function deleteItem(data: FormData) {
"use server";
const id = data.get("id") as string;
if (!id) return;
await db.sellable.delete({ where: { id } });
revalidatePath("/items");
}
return (
<main className="flex min-h-screen items-center justify-center bg-linear-to-b from-[#2e026d] to-[#7f3b3b] p-4 text-white">
<div className="w-full max-w-4xl space-y-8 rounded-lg bg-white/10 p-6 backdrop-blur-md">
<h1 className="text-4xl font-extrabold">
Manage <span className="text-[hsl(280,100%,70%)]">Items</span>
</h1>
{/* Add new item */}
<section>
<h2 className="mb-2 text-2xl font-semibold">Add New Item</h2>
<form action={addItem} className="flex flex-col gap-4">
<label className="flex flex-col text-sm">
Item Name
<input
type="text"
name="item_name"
placeholder="Item Name"
required
className="rounded border border-white/30 bg-white/10 p-2 text-sm text-white placeholder-white/70"
/>
</label>
<label className="flex flex-col text-sm">
Price
<input
type="number"
step="0.01"
name="price"
placeholder="Price"
required
className="rounded border border-white/30 bg-white/10 p-2 text-sm text-white placeholder-white/70"
/>
</label>
<label className="flex flex-col text-sm">
Amount
<input
type="number"
name="amount"
placeholder="Amount"
required
className="rounded border border-white/30 bg-white/10 p-2 text-sm text-white placeholder-white/70"
/>
</label>
<label className="flex flex-col text-sm">
Shop
<select
name="shopId"
required
className="rounded border border-white/30 bg-white/10 p-2 text-sm text-white"
>
{uniqueShops.map((shop) => (
<option key={shop.id} value={shop.id}>
{shop.label}
</option>
))}
</select>
</label>
<button
type="submit"
className="rounded bg-[hsl(280,100%,70%)] p-2 text-sm font-semibold"
>
Add Item
</button>
</form>
</section>
{/* Existing items in a grid */}
<section>
<h2 className="mb-2 text-2xl font-semibold">Your Items</h2>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
{items.map((item) => (
<div
key={item.id}
className="space-y-2 rounded bg-white/10 p-4 text-sm backdrop-blur-sm"
>
{/* Update form */}
<form action={updateItem} className="flex flex-col gap-2">
<input type="hidden" name="id" value={item.id} />
<label className="flex flex-col">
Name
<input
type="text"
name="item_name"
defaultValue={item.item_name}
className="rounded border border-white/30 bg-white/10 p-1 text-sm text-white"
/>
</label>
<label className="flex flex-col">
Price
<input
type="number"
step="0.01"
name="price"
defaultValue={item.price}
className="rounded border border-white/30 bg-white/10 p-1 text-sm text-white"
/>
</label>
<label className="flex flex-col">
Amount
<input
type="number"
name="amount"
defaultValue={item.amount}
className="rounded border border-white/30 bg-white/10 p-1 text-sm text-white"
/>
</label>
<button
type="submit"
className="rounded bg-green-600 p-1 text-sm font-semibold"
>
Update
</button>
</form>
{/* Delete button */}
<form action={deleteItem}>
<input type="hidden" name="id" value={item.id} />
<button
type="submit"
className="mt-1 w-full rounded bg-red-600 p-1 text-sm font-semibold"
>
Delete
</button>
</form>
<p className="text-xs text-white/70">Shop: {item.shop.label}</p>
</div>
))}
</div>
</section>
</div>
</main>
);
}
+7 -35
View File
@@ -1,37 +1,9 @@
import Link from "next/link";
import { auth } from "~/server/auth";
import type { Session } from "next-auth";
import HomeClient from "~/components/HomeClient";
export default function HomePage() {
return (
<main className="flex min-h-screen flex-col items-center justify-center bg-gradient-to-b from-[#2e026d] to-[#15162c] text-white">
<div className="container flex flex-col items-center justify-center gap-12 px-4 py-16">
<h1 className="text-5xl font-extrabold tracking-tight text-white sm:text-[5rem]">
Create <span className="text-[hsl(280,100%,70%)]">T3</span> App
</h1>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:gap-8">
<Link
className="flex max-w-xs flex-col gap-4 rounded-xl bg-white/10 p-4 text-white hover:bg-white/20"
href="https://create.t3.gg/en/usage/first-steps"
target="_blank"
>
<h3 className="text-2xl font-bold">First Steps </h3>
<div className="text-lg">
Just the basics - Everything you need to know to set up your
database and authentication.
</div>
</Link>
<Link
className="flex max-w-xs flex-col gap-4 rounded-xl bg-white/10 p-4 text-white hover:bg-white/20"
href="https://create.t3.gg/en/introduction"
target="_blank"
>
<h3 className="text-2xl font-bold">Documentation </h3>
<div className="text-lg">
Learn more about Create T3 App, the libraries it uses, and how to
deploy it.
</div>
</Link>
</div>
</div>
</main>
);
export default async function HomePage() {
const session: Session | null = await auth();
return <HomeClient session={session} />;
}
+48
View File
@@ -0,0 +1,48 @@
"use client";
import { useState } from "react";
import Link from "next/link";
import Items from "~/components/sellable_items";
import Search from "~/components/search";
import type { Session } from "next-auth";
type Props = {
session: Session | null;
};
export default function HomeClient({ session }: Props) {
const [query, setQuery] = useState("");
return (
<main className="flex min-h-screen flex-col items-center justify-center bg-linear-to-b from-[#2e026d] to-[#7f3b3b] text-white">
<div className="absolute top-4 right-4 flex items-center gap-4">
<Search query={query} setQuery={setQuery} />
{session?.user ? (
<Link
href="/api/auth/signout"
className="rounded-full bg-white/10 px-4 py-2 font-semibold transition hover:bg-white/20"
>
Sign out
</Link>
) : (
<Link
href="/api/auth/signin"
className="rounded-full bg-white/10 px-4 py-2 font-semibold transition hover:bg-white/20"
>
Sign in
</Link>
)}
</div>
<div className="container flex flex-col items-center gap-12 px-4 py-16">
<h1 className="text-5xl font-extrabold tracking-tight">
Suchodupin <span className="text-[hsl(280,100%,70%)]">MC</span> Shop
</h1>
{/* ✅ query flows down */}
<Items query={query} />
</div>
</main>
);
}
+117
View File
@@ -0,0 +1,117 @@
"use client";
import { useState } from "react";
type Shop = {
id: number;
label: string;
};
export default function EditableShops({ shops }: { shops: Shop[] }) {
const [items, setItems] = useState<Shop[]>(shops);
const [saving, setSaving] = useState(false);
function addShop() {
setItems((prev) => [
...prev,
{ id: 0, label: "" }, // user must set ID
]);
}
function removeShop(id: number) {
setItems((prev) => prev.filter((s) => s.id !== id));
}
function updateId(oldId: number, newId: number) {
setItems((prev) =>
prev.map((s) => (s.id === oldId ? { ...s, id: newId } : s)),
);
}
function updateLabel(id: number, value: string) {
setItems((prev) =>
prev.map((s) => (s.id === id ? { ...s, label: value } : s)),
);
}
function hasDuplicateIds() {
const ids = items.map((s) => s.id).filter((id) => id !== 0);
return new Set(ids).size !== ids.length;
}
async function save() {
if (hasDuplicateIds()) {
alert("Shop IDs must be unique");
return;
}
setSaving(true);
await fetch("/api/user", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
shops: items.filter((s) => s.id > 0),
}),
});
setSaving(false);
}
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<p className="font-semibold">Shops</p>
<button
onClick={addShop}
className="rounded-md bg-purple-600 px-3 py-1 text-sm font-semibold hover:bg-purple-700"
>
+ Add
</button>
</div>
{items.map((shop, index) => (
<div
key={`${shop.id}-${index}`}
className="flex items-start gap-2 rounded-md bg-black/30 p-3"
>
<div className="flex-1 space-y-2">
<label className="text-xs text-white/60 uppercase">
Shop {index + 1}
</label>
<input
type="number"
placeholder="Shop ID"
value={shop.id || ""}
onChange={(e) => updateId(shop.id, Number(e.target.value))}
className="w-full rounded-md bg-black/40 p-2 text-white outline-none focus:ring-2 focus:ring-purple-400"
/>
<input
placeholder="Shop label"
value={shop.label}
onChange={(e) => updateLabel(shop.id, e.target.value)}
className="w-full rounded-md bg-black/40 p-2 text-white outline-none focus:ring-2 focus:ring-purple-400"
/>
</div>
<button
onClick={() => removeShop(shop.id)}
className="mt-6 text-sm text-red-400 hover:text-red-300"
>
Remove
</button>
</div>
))}
<button
onClick={save}
disabled={saving}
className="w-full rounded-md bg-purple-600 py-2 font-semibold hover:bg-purple-700 disabled:opacity-50"
>
{saving ? "Saving..." : "Save Shops"}
</button>
</div>
);
}
+29
View File
@@ -0,0 +1,29 @@
"use client";
type Props = {
query: string;
setQuery: (value: string) => void;
};
export default function Search({ query, setQuery }: Props) {
return (
<div className="relative">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search items..."
className="w-48 rounded-full bg-white/10 px-4 py-2 text-sm text-white placeholder-white/60 transition outline-none focus:bg-white/20 focus:ring-2 focus:ring-white/30"
/>
{query && (
<button
onClick={() => setQuery("")}
aria-label="Clear search"
className="absolute top-1/2 right-2 -translate-y-1/2 text-white/60 hover:text-white"
>
</button>
)}
</div>
);
}
+88
View File
@@ -0,0 +1,88 @@
"use client";
import { useEffect, useState } from "react";
type ApiItem = {
id: string;
item_name: string;
amount: number;
price: number;
enabled: boolean;
shop: {
label: string;
};
item: {
stock: number;
};
};
const formatName = (name: string) => {
const parts = name.split(":");
if (!parts[1]) return "";
return parts[1]
.split("_")
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join(" ");
};
const getImage = (name: string) => {
const [mod, item] = name.split(":");
return `/textures/${mod}/${item}.png`;
};
const Items = ({ query }: { query: string }) => {
const [items, setItems] = useState<ApiItem[]>([]);
useEffect(() => {
const loadItems = async () => {
try {
const res = await fetch("/api/items");
const data = (await res.json()) as ApiItem[];
setItems(data.filter((item) => item.enabled));
} catch (err) {
console.error("Failed to load items", err);
}
};
void loadItems();
}, []);
const filteredItems = items.filter((item) =>
formatName(item.item_name).toLowerCase().includes(query.toLowerCase()),
);
return (
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4">
{filteredItems.map((item) => (
<div
key={item.id}
className="flex gap-4 rounded-xl bg-white/10 p-4 text-white hover:bg-white/20"
>
<img
src={getImage(item.item_name)}
alt={item.item_name}
className="item-img h-12 w-12"
/>
<div className="w-70">
<h2 className="text-xl font-bold">{formatName(item.item_name)}</h2>
<p className="text-sm">{item.item.stock} available</p>
<p className="text-xs text-white/70">Shop: {item.shop.label}</p>
</div>
<div className="flex flex-col items-end">
<button className="rounded bg-blue-500 px-4 py-2 font-bold text-white hover:bg-blue-700">
Buy
</button>
<h3 className="w-fit self-center text-xl font-bold">
{item.price}$/{item.amount}
</h3>
</div>
</div>
))}
</div>
);
};
export default Items;