balls
This commit is contained in:
parent
34cd43332b
commit
4199f50bac
34
README.md
34
README.md
@ -1,34 +1,8 @@
|
||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||
# ASSCO
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
# Build
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
npm run build
|
||||
npm start
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||
|
||||
37
src/app/api/webhook/route.ts
Normal file
37
src/app/api/webhook/route.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
|
||||
const webhookUrl = process.env.DISCORD_WEBHOOK_URL;
|
||||
|
||||
if (!webhookUrl) {
|
||||
console.error("DISCORD_WEBHOOK_URL environment variable not set");
|
||||
return NextResponse.json(
|
||||
{ error: "Webhook not configured" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
|
||||
const response = await fetch(webhookUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Discord API responded with status ${response.status}`);
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error("Webhook error:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to send webhook" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1 +1,42 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
body {
|
||||
background-color: #111827;
|
||||
color: #ffffff;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
}
|
||||
|
||||
input:focus,
|
||||
select:focus {
|
||||
outline: none;
|
||||
border-color: #22c55e;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.text-green-400 {
|
||||
color: #4ade80;
|
||||
}
|
||||
|
||||
.bg-green-600 {
|
||||
background-color: #16a34a;
|
||||
}
|
||||
|
||||
.bg-green-700 {
|
||||
background-color: #15803d;
|
||||
}
|
||||
|
||||
.bg-green-800 {
|
||||
background-color: #166534;
|
||||
}
|
||||
|
||||
.hover\:bg-green-700:hover {
|
||||
background-color: #15803d;
|
||||
}
|
||||
|
||||
@ -2,8 +2,8 @@ import type { Metadata } from "next";
|
||||
import "./globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
title: "ASS AB - Allt vi Skapar Säljer vi",
|
||||
description: "Beställ Minecraft-föremål från ASS AB",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
|
||||
108
src/app/page.tsx
108
src/app/page.tsx
@ -1,7 +1,107 @@
|
||||
export default function Home() {
|
||||
"use client";
|
||||
|
||||
import { CartProvider, useCart } from "../contexts/CartContext";
|
||||
import ProductSelection from "../components/ProductSelection";
|
||||
import CartView from "../components/CartView";
|
||||
import DeliveryDetails from "../components/DeliveryDetails";
|
||||
|
||||
function OrderApp() {
|
||||
const { state, dispatch } = useCart();
|
||||
|
||||
const steps = ["Lägg till produkter", "Kundvagn", "Leveransuppgifter"];
|
||||
|
||||
const nextStep = () => {
|
||||
if (state.currentStep < 2) {
|
||||
dispatch({ type: "SET_STEP", payload: state.currentStep + 1 });
|
||||
}
|
||||
};
|
||||
|
||||
const prevStep = () => {
|
||||
if (state.currentStep > 0) {
|
||||
dispatch({ type: "SET_STEP", payload: state.currentStep - 1 });
|
||||
}
|
||||
};
|
||||
|
||||
const canProceed = () => {
|
||||
if (state.currentStep === 0) return state.items.length > 0;
|
||||
if (state.currentStep === 1) return state.items.length > 0;
|
||||
return true;
|
||||
};
|
||||
|
||||
return (
|
||||
<main>
|
||||
<div>Hello world!</div>
|
||||
</main>
|
||||
<div className="min-h-screen bg-gray-900">
|
||||
<header className="bg-green-700 text-white py-6">
|
||||
<div className="container mx-auto px-4">
|
||||
<h1 className="text-3xl font-bold text-center">
|
||||
ASS AB - Allt vi Skapar Säljer vi
|
||||
</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="mb-8">
|
||||
<div className="flex justify-center mb-4">
|
||||
{steps.map((step, index) => (
|
||||
<div key={index} className="flex items-center">
|
||||
<div
|
||||
className={`px-4 py-2 rounded-full ${
|
||||
index === state.currentStep
|
||||
? "bg-green-600 text-white"
|
||||
: index < state.currentStep
|
||||
? "bg-green-800 text-white"
|
||||
: "bg-gray-600 text-gray-300"
|
||||
}`}
|
||||
>
|
||||
{step}
|
||||
</div>
|
||||
{index < steps.length - 1 && (
|
||||
<div className="w-8 h-0.5 bg-gray-600 mx-2"></div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="max-w-4xl mx-auto">
|
||||
{state.currentStep === 0 && <ProductSelection />}
|
||||
{state.currentStep === 1 && <CartView />}
|
||||
{state.currentStep === 2 && <DeliveryDetails />}
|
||||
|
||||
<div className="flex justify-between mt-8">
|
||||
<button
|
||||
onClick={prevStep}
|
||||
disabled={state.currentStep === 0}
|
||||
className="px-6 py-2 bg-gray-600 hover:bg-gray-700 disabled:bg-gray-800 disabled:cursor-not-allowed text-white rounded transition-colors"
|
||||
>
|
||||
Tillbaka
|
||||
</button>
|
||||
|
||||
{state.currentStep < 2 && (
|
||||
<button
|
||||
onClick={nextStep}
|
||||
disabled={!canProceed()}
|
||||
className="px-6 py-2 bg-green-600 hover:bg-green-700 disabled:bg-gray-600 disabled:cursor-not-allowed text-white rounded transition-colors"
|
||||
>
|
||||
Nästa
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer className="bg-gray-800 text-gray-400 py-6 mt-12">
|
||||
<div className="container mx-auto px-4 text-center">
|
||||
<p>© ASS co 2025 - Allt vi Skapar Säljer vi</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<CartProvider>
|
||||
<OrderApp />
|
||||
</CartProvider>
|
||||
);
|
||||
}
|
||||
|
||||
157
src/components/CartView.tsx
Normal file
157
src/components/CartView.tsx
Normal file
@ -0,0 +1,157 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useCart } from "../contexts/CartContext";
|
||||
import { formatDiamonds } from "../utils/formatDiamonds";
|
||||
import discountCodes from "../data/discountCodes.json";
|
||||
|
||||
export default function CartView() {
|
||||
const { state, dispatch } = useCart();
|
||||
const [discountCode, setDiscountCode] = useState("");
|
||||
|
||||
const calculateItemTotal = (item: (typeof state.items)[0]) => {
|
||||
let basePrice = item.product.pricePerStack * item.quantity;
|
||||
|
||||
if (item.product.id === "cobblestone" && item.variant !== "Cobblestone") {
|
||||
basePrice = item.quantity / 8;
|
||||
}
|
||||
|
||||
if (item.deliveryOption === "shulker") {
|
||||
basePrice += 1;
|
||||
}
|
||||
|
||||
return Math.round(basePrice);
|
||||
};
|
||||
|
||||
const subtotal = state.items.reduce(
|
||||
(sum, item) => sum + calculateItemTotal(item),
|
||||
0,
|
||||
);
|
||||
const discount = Math.round(subtotal * (state.discountPercentage / 100));
|
||||
const total = subtotal - discount;
|
||||
|
||||
const applyDiscountCode = () => {
|
||||
const code = discountCode.toLowerCase();
|
||||
const discountInfo = discountCodes[code as keyof typeof discountCodes];
|
||||
|
||||
if (discountInfo) {
|
||||
dispatch({
|
||||
type: "SET_DISCOUNT",
|
||||
payload: { code: discountCode, percentage: discountInfo.percentage },
|
||||
});
|
||||
} else {
|
||||
dispatch({ type: "SET_DISCOUNT", payload: { code: "", percentage: 0 } });
|
||||
}
|
||||
};
|
||||
|
||||
const removeItem = (index: number) => {
|
||||
dispatch({ type: "REMOVE_ITEM", payload: index });
|
||||
};
|
||||
|
||||
const updateQuantity = (index: number, quantity: number) => {
|
||||
if (quantity <= 0) {
|
||||
removeItem(index);
|
||||
} else {
|
||||
dispatch({ type: "UPDATE_QUANTITY", payload: { index, quantity } });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-gray-800 p-6 rounded-lg border border-gray-700">
|
||||
<h2 className="text-2xl font-bold text-green-400 mb-6">Kundvagn</h2>
|
||||
|
||||
{state.items.length === 0 ? (
|
||||
<p className="text-gray-400">Kundvagnen är tom</p>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{state.items.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-gray-700 p-4 rounded border border-gray-600"
|
||||
>
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex-1">
|
||||
<h3 className="text-white font-semibold">
|
||||
{item.product.name}
|
||||
{item.variant && ` (${item.variant})`}
|
||||
</h3>
|
||||
<p className="text-gray-400 text-sm">
|
||||
Leverans:{" "}
|
||||
{item.deliveryOption === "shulker"
|
||||
? "Shulker (+1 Dia)"
|
||||
: item.deliveryOption === "pickup"
|
||||
? "Pickup (-10%)"
|
||||
: "Kistor"}
|
||||
</p>
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<span className="text-gray-300">Antal:</span>
|
||||
<input
|
||||
type="number"
|
||||
value={item.quantity}
|
||||
onChange={(e) =>
|
||||
updateQuantity(index, parseInt(e.target.value) || 0)
|
||||
}
|
||||
min="0"
|
||||
className="w-20 bg-gray-600 text-white p-1 rounded border border-gray-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-white font-semibold">
|
||||
{formatDiamonds(calculateItemTotal(item))}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => removeItem(index)}
|
||||
className="text-red-400 hover:text-red-300 mt-2 text-sm"
|
||||
>
|
||||
Ta bort
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="border-t border-gray-600 pt-4 space-y-2">
|
||||
<div className="flex justify-between text-gray-300">
|
||||
<span>Subtotal:</span>
|
||||
<span>{formatDiamonds(subtotal)}</span>
|
||||
</div>
|
||||
|
||||
{state.discountPercentage > 0 && (
|
||||
<div className="flex justify-between text-green-400">
|
||||
<span>Rabatt ({state.discountCode}):</span>
|
||||
<span>
|
||||
-{formatDiamonds(discount)} ({state.discountPercentage}%)
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-between text-white font-bold text-lg border-t border-gray-600 pt-2">
|
||||
<span>Totalt:</span>
|
||||
<span>{formatDiamonds(total)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-600 pt-4">
|
||||
<label className="block text-gray-300 mb-2">Rabattkod:</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={discountCode}
|
||||
onChange={(e) => setDiscountCode(e.target.value)}
|
||||
placeholder="Skriv rabattkod"
|
||||
className="flex-1 bg-gray-700 text-white p-3 rounded border border-gray-600 focus:border-green-400 focus:outline-none"
|
||||
/>
|
||||
<button
|
||||
onClick={applyDiscountCode}
|
||||
className="bg-green-600 hover:bg-green-700 text-white px-6 py-3 rounded transition-colors"
|
||||
>
|
||||
Använd kod
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
218
src/components/DeliveryDetails.tsx
Normal file
218
src/components/DeliveryDetails.tsx
Normal file
@ -0,0 +1,218 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useCart } from "../contexts/CartContext";
|
||||
import { formatDiamonds } from "../utils/formatDiamonds";
|
||||
|
||||
export default function DeliveryDetails() {
|
||||
const { state, dispatch } = useCart();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const calculateTotal = () => {
|
||||
let total = 0;
|
||||
|
||||
state.items.forEach((item) => {
|
||||
let itemPrice = item.product.pricePerStack * item.quantity;
|
||||
|
||||
if (item.product.id === "cobblestone" && item.variant !== "Cobblestone") {
|
||||
itemPrice = item.quantity / 8;
|
||||
}
|
||||
|
||||
if (item.deliveryOption === "shulker") {
|
||||
itemPrice += 1;
|
||||
}
|
||||
|
||||
total += Math.round(itemPrice);
|
||||
});
|
||||
|
||||
const hasPickup = state.items.some(
|
||||
(item) => item.deliveryOption === "pickup",
|
||||
);
|
||||
if (hasPickup) {
|
||||
total = Math.round(total * 0.9);
|
||||
}
|
||||
|
||||
if (state.discountPercentage > 0) {
|
||||
total = Math.round(total * (1 - state.discountPercentage / 100));
|
||||
}
|
||||
|
||||
return total;
|
||||
};
|
||||
|
||||
const submitOrder = async () => {
|
||||
if (!state.playerName || (!hasPickup && !state.coords)) {
|
||||
alert("Vänligen fyll i alla fält");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
|
||||
const finalCoords = hasPickup ? "x:2100 z:1500" : state.coords;
|
||||
|
||||
const orderData = {
|
||||
embeds: [
|
||||
{
|
||||
title: "🛒 Ny beställning från ASS AB",
|
||||
color: 0x22c55e,
|
||||
fields: [
|
||||
{
|
||||
name: "👤 Spelare",
|
||||
value: state.playerName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "📍 Koordinater",
|
||||
value: finalCoords,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "💎 Total kostnad",
|
||||
value: formatDiamonds(calculateTotal()),
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "📦 Beställda varor",
|
||||
value: state.items
|
||||
.map((item) => {
|
||||
const itemTotal = (() => {
|
||||
let price = item.product.pricePerStack * item.quantity;
|
||||
if (
|
||||
item.product.id === "cobblestone" &&
|
||||
item.variant !== "Cobblestone"
|
||||
) {
|
||||
price = item.quantity / 8;
|
||||
}
|
||||
if (item.deliveryOption === "shulker") {
|
||||
price += 1;
|
||||
}
|
||||
return Math.round(price);
|
||||
})();
|
||||
|
||||
return `• ${item.product.name}${item.variant ? ` (${item.variant})` : ""}: ${item.quantity} stacks - ${formatDiamonds(itemTotal)}\n Leverans: ${item.deliveryOption === "shulker" ? "Shulker (+1 Dia)" : item.deliveryOption === "pickup" ? "Pickup (-10%)" : "Kistor"}`;
|
||||
})
|
||||
.join("\n"),
|
||||
inline: false,
|
||||
},
|
||||
],
|
||||
footer: {
|
||||
text: `Beställning skickad ${new Date().toLocaleString("sv-SE")}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/webhook", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(orderData),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert("Beställning skickad! Vi kontaktar dig snart.");
|
||||
dispatch({ type: "CLEAR_CART" });
|
||||
dispatch({ type: "SET_STEP", payload: 0 });
|
||||
dispatch({ type: "SET_PLAYER_NAME", payload: "" });
|
||||
dispatch({ type: "SET_COORDS", payload: "" });
|
||||
} else {
|
||||
alert("Något gick fel. Försök igen.");
|
||||
}
|
||||
} catch (error) {
|
||||
alert("Något gick fel. Försök igen.");
|
||||
console.error("Error:", error);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const hasPickup = state.items.some(
|
||||
(item) => item.deliveryOption === "pickup",
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="bg-gray-800 p-6 rounded-lg border border-gray-700">
|
||||
<h2 className="text-2xl font-bold text-green-400 mb-6">
|
||||
Leveransuppgifter
|
||||
</h2>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<label className="block text-gray-300 mb-2">Namn:</label>
|
||||
<input
|
||||
type="text"
|
||||
value={state.playerName}
|
||||
onChange={(e) =>
|
||||
dispatch({ type: "SET_PLAYER_NAME", payload: e.target.value })
|
||||
}
|
||||
placeholder="Spelarnamn"
|
||||
className="w-full bg-gray-700 text-white p-3 rounded border border-gray-600 focus:border-green-400 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-gray-300 mb-2">
|
||||
Koordinater {hasPickup && "(pickup på x:2100 z:1500)"}:
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={hasPickup ? "x:2100 z:1500" : state.coords}
|
||||
onChange={(e) =>
|
||||
!hasPickup &&
|
||||
dispatch({ type: "SET_COORDS", payload: e.target.value })
|
||||
}
|
||||
placeholder={hasPickup ? "x:2100 z:1500" : "Ex: 123 64 -456"}
|
||||
disabled={hasPickup}
|
||||
className={`w-full bg-gray-700 text-white p-3 rounded border border-gray-600 focus:border-green-400 focus:outline-none ${hasPickup ? "opacity-75" : ""}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-700 p-4 rounded border border-gray-600">
|
||||
<h3 className="text-white font-semibold mb-3">Ordersammanfattning</h3>
|
||||
<div className="space-y-2 text-sm">
|
||||
{state.items.map((item, index) => (
|
||||
<div key={index} className="flex justify-between text-gray-300">
|
||||
<span>
|
||||
{item.product.name}
|
||||
{item.variant ? ` (${item.variant})` : ""} x{item.quantity}
|
||||
</span>
|
||||
<span>
|
||||
{(() => {
|
||||
let price = item.product.pricePerStack * item.quantity;
|
||||
if (
|
||||
item.product.id === "cobblestone" &&
|
||||
item.variant !== "Cobblestone"
|
||||
) {
|
||||
price = item.quantity / 8;
|
||||
}
|
||||
if (item.deliveryOption === "shulker") {
|
||||
price += 1;
|
||||
}
|
||||
return formatDiamonds(Math.round(price));
|
||||
})()}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
<div className="border-t border-gray-600 pt-2 font-bold text-white">
|
||||
<div className="flex justify-between">
|
||||
<span>Total:</span>
|
||||
<span>{formatDiamonds(calculateTotal())}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={submitOrder}
|
||||
disabled={
|
||||
!state.playerName || (!hasPickup && !state.coords) || isSubmitting
|
||||
}
|
||||
className="w-full bg-green-600 hover:bg-green-700 disabled:bg-gray-600 disabled:cursor-not-allowed text-white font-semibold py-3 px-4 rounded transition-colors"
|
||||
>
|
||||
{isSubmitting ? "Skickar beställning..." : "Beställ"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
126
src/components/ProductSelection.tsx
Normal file
126
src/components/ProductSelection.tsx
Normal file
@ -0,0 +1,126 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { products } from "../data/products";
|
||||
import { useCart } from "../contexts/CartContext";
|
||||
import type { Product, CartItem } from "../types/product";
|
||||
|
||||
export default function ProductSelection() {
|
||||
const { dispatch } = useCart();
|
||||
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
|
||||
const [quantity, setQuantity] = useState(1);
|
||||
const [deliveryOption, setDeliveryOption] = useState<
|
||||
"shulker" | "chests" | "pickup"
|
||||
>("chests");
|
||||
const [variant, setVariant] = useState("");
|
||||
|
||||
const handleAddToCart = () => {
|
||||
if (!selectedProduct) return;
|
||||
|
||||
const item: CartItem = {
|
||||
product: selectedProduct,
|
||||
quantity,
|
||||
unit: "stacks",
|
||||
deliveryOption,
|
||||
variant: variant || undefined,
|
||||
};
|
||||
|
||||
dispatch({ type: "ADD_ITEM", payload: item });
|
||||
|
||||
setSelectedProduct(null);
|
||||
setQuantity(1);
|
||||
setVariant("");
|
||||
};
|
||||
|
||||
const getProductPrice = (product: Product) => {
|
||||
if (product.defaultStacksPerDia) {
|
||||
return `${product.defaultStacksPerDia} Stacks = 1 Dia`;
|
||||
}
|
||||
return `1 Stack = ${product.pricePerStack} Dia`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-gray-800 p-6 rounded-lg border border-gray-700">
|
||||
<h2 className="text-2xl font-bold text-green-400 mb-6">
|
||||
Lägg till produkt
|
||||
</h2>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<label className="block text-gray-300 mb-2">Produkt:</label>
|
||||
<select
|
||||
value={selectedProduct?.id || ""}
|
||||
onChange={(e) => {
|
||||
const product = products.find((p) => p.id === e.target.value);
|
||||
setSelectedProduct(product || null);
|
||||
setVariant("");
|
||||
}}
|
||||
className="w-full bg-gray-700 text-white p-3 rounded border border-gray-600 focus:border-green-400 focus:outline-none"
|
||||
>
|
||||
{products.map((product) => (
|
||||
<option key={product.id} value={product.id}>
|
||||
{product.name} ({getProductPrice(product)})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{selectedProduct?.variants && (
|
||||
<div>
|
||||
<label className="block text-gray-300 mb-2">Variant:</label>
|
||||
<select
|
||||
value={variant}
|
||||
onChange={(e) => setVariant(e.target.value)}
|
||||
className="w-full bg-gray-700 text-white p-3 rounded border border-gray-600 focus:border-green-400 focus:outline-none"
|
||||
>
|
||||
<option value="">Välj variant</option>
|
||||
{selectedProduct.variants.map((variant) => (
|
||||
<option key={variant} value={variant}>
|
||||
{variant}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-gray-300 mb-2">Antal stackar:</label>
|
||||
<input
|
||||
type="number"
|
||||
value={quantity}
|
||||
onChange={(e) =>
|
||||
setQuantity(Math.max(1, parseInt(e.target.value) || 1))
|
||||
}
|
||||
min="1"
|
||||
className="w-full bg-gray-700 text-white p-3 rounded border border-gray-600 focus:border-green-400 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-gray-300 mb-2">Leveransbehållare:</label>
|
||||
<select
|
||||
value={deliveryOption}
|
||||
onChange={(e) =>
|
||||
setDeliveryOption(
|
||||
e.target.value as "shulker" | "chests" | "pickup",
|
||||
)
|
||||
}
|
||||
className="w-full bg-gray-700 text-white p-3 rounded border border-gray-600 focus:border-green-400 focus:outline-none"
|
||||
>
|
||||
<option value="chests">Kistor (standard)</option>
|
||||
<option value="shulker">Shulker (+1 Dia)</option>
|
||||
<option value="pickup">Pickup (-10% rabatt)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleAddToCart}
|
||||
disabled={!selectedProduct || (selectedProduct.variants && !variant)}
|
||||
className="w-full bg-green-600 hover:bg-green-700 disabled:bg-gray-600 disabled:cursor-not-allowed text-white font-semibold py-3 px-4 rounded transition-colors"
|
||||
>
|
||||
Lägg till i kundvagn
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
117
src/contexts/CartContext.tsx
Normal file
117
src/contexts/CartContext.tsx
Normal file
@ -0,0 +1,117 @@
|
||||
"use client";
|
||||
|
||||
import React, { createContext, useContext, useReducer, ReactNode } from "react";
|
||||
import type { CartItem } from "../types/product";
|
||||
|
||||
interface CartState {
|
||||
items: CartItem[];
|
||||
discountCode: string;
|
||||
discountPercentage: number;
|
||||
playerName: string;
|
||||
coords: string;
|
||||
currentStep: number;
|
||||
}
|
||||
|
||||
type CartAction =
|
||||
| { type: "ADD_ITEM"; payload: CartItem }
|
||||
| { type: "REMOVE_ITEM"; payload: number }
|
||||
| { type: "UPDATE_QUANTITY"; payload: { index: number; quantity: number } }
|
||||
| { type: "CLEAR_CART" }
|
||||
| { type: "SET_DISCOUNT"; payload: { code: string; percentage: number } }
|
||||
| { type: "SET_PLAYER_NAME"; payload: string }
|
||||
| { type: "SET_COORDS"; payload: string }
|
||||
| { type: "SET_STEP"; payload: number };
|
||||
|
||||
const initialState: CartState = {
|
||||
items: [],
|
||||
discountCode: "",
|
||||
discountPercentage: 0,
|
||||
playerName: "",
|
||||
coords: "",
|
||||
currentStep: 0,
|
||||
};
|
||||
|
||||
function cartReducer(state: CartState, action: CartAction): CartState {
|
||||
switch (action.type) {
|
||||
case "ADD_ITEM":
|
||||
const existingIndex = state.items.findIndex(
|
||||
(item) =>
|
||||
item.product.id === action.payload.product.id &&
|
||||
item.variant === action.payload.variant &&
|
||||
item.unit === action.payload.unit &&
|
||||
item.deliveryOption === action.payload.deliveryOption,
|
||||
);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
const newItems = [...state.items];
|
||||
newItems[existingIndex] = {
|
||||
...newItems[existingIndex],
|
||||
quantity: newItems[existingIndex].quantity + action.payload.quantity,
|
||||
};
|
||||
return { ...state, items: newItems };
|
||||
}
|
||||
|
||||
return { ...state, items: [...state.items, action.payload] };
|
||||
|
||||
case "REMOVE_ITEM":
|
||||
return {
|
||||
...state,
|
||||
items: state.items.filter((_, index) => index !== action.payload),
|
||||
};
|
||||
|
||||
case "UPDATE_QUANTITY":
|
||||
const updatedItems = [...state.items];
|
||||
if (updatedItems[action.payload.index]) {
|
||||
updatedItems[action.payload.index] = {
|
||||
...updatedItems[action.payload.index],
|
||||
quantity: action.payload.quantity,
|
||||
};
|
||||
}
|
||||
return { ...state, items: updatedItems };
|
||||
|
||||
case "CLEAR_CART":
|
||||
return { ...state, items: [] };
|
||||
|
||||
case "SET_DISCOUNT":
|
||||
return {
|
||||
...state,
|
||||
discountCode: action.payload.code,
|
||||
discountPercentage: action.payload.percentage,
|
||||
};
|
||||
|
||||
case "SET_PLAYER_NAME":
|
||||
return { ...state, playerName: action.payload };
|
||||
|
||||
case "SET_COORDS":
|
||||
return { ...state, coords: action.payload };
|
||||
|
||||
case "SET_STEP":
|
||||
return { ...state, currentStep: action.payload };
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
const CartContext = createContext<{
|
||||
state: CartState;
|
||||
dispatch: React.Dispatch<CartAction>;
|
||||
} | null>(null);
|
||||
|
||||
export function CartProvider({ children }: { children: ReactNode }) {
|
||||
const [state, dispatch] = useReducer(cartReducer, initialState);
|
||||
|
||||
return (
|
||||
<CartContext.Provider value={{ state, dispatch }}>
|
||||
{children}
|
||||
</CartContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useCart() {
|
||||
const context = useContext(CartContext);
|
||||
if (!context) {
|
||||
throw new Error("useCart must be used within a CartProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
1
src/data/discountCodes.json
Normal file
1
src/data/discountCodes.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
||||
42
src/data/products.ts
Normal file
42
src/data/products.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import type { Product } from "../types/product";
|
||||
|
||||
export const products: Product[] = [
|
||||
{
|
||||
id: "spruce-log",
|
||||
name: "Spruce Log",
|
||||
pricePerStack: 1,
|
||||
},
|
||||
{
|
||||
id: "birch-log",
|
||||
name: "Birch Log",
|
||||
pricePerStack: 1,
|
||||
},
|
||||
{
|
||||
id: "oak-log",
|
||||
name: "Oak Log",
|
||||
pricePerStack: 1,
|
||||
},
|
||||
{
|
||||
id: "jungle-log",
|
||||
name: "Jungle Log",
|
||||
pricePerStack: 1,
|
||||
},
|
||||
{
|
||||
id: "cobblestone",
|
||||
name: "Cobblestone",
|
||||
pricePerStack: 1 / 16,
|
||||
defaultStacksPerDia: 16,
|
||||
variants: ["Cobblestone", "Stone", "Stone Bricks"],
|
||||
},
|
||||
{
|
||||
id: "sand",
|
||||
name: "Sand",
|
||||
pricePerStack: 1,
|
||||
},
|
||||
{
|
||||
id: "gunpowder",
|
||||
name: "Gunpowder",
|
||||
pricePerStack: 1 / 4,
|
||||
defaultStacksPerDia: 4,
|
||||
},
|
||||
];
|
||||
65
src/types/product.ts
Normal file
65
src/types/product.ts
Normal file
@ -0,0 +1,65 @@
|
||||
export interface Product {
|
||||
id: string;
|
||||
name: string;
|
||||
pricePerStack: number;
|
||||
variants?: string[];
|
||||
defaultStacksPerDia?: number;
|
||||
}
|
||||
|
||||
export interface CartItem {
|
||||
product: Product;
|
||||
quantity: number;
|
||||
variant?: string;
|
||||
unit: "stacks" | "shulkers" | "double-chests";
|
||||
deliveryOption: "shulker" | "chests" | "pickup";
|
||||
}
|
||||
|
||||
export interface Order {
|
||||
items: CartItem[];
|
||||
total: number;
|
||||
discount: number;
|
||||
discountCode?: string;
|
||||
playerName: string;
|
||||
coords: string;
|
||||
}
|
||||
|
||||
export const products: Product[] = [
|
||||
{
|
||||
id: "spruce-log",
|
||||
name: "Spruce Log",
|
||||
pricePerStack: 1,
|
||||
},
|
||||
{
|
||||
id: "birch-log",
|
||||
name: "Birch Log",
|
||||
pricePerStack: 1,
|
||||
},
|
||||
{
|
||||
id: "oak-log",
|
||||
name: "Oak Log",
|
||||
pricePerStack: 1,
|
||||
},
|
||||
{
|
||||
id: "jungle-log",
|
||||
name: "Jungle Log",
|
||||
pricePerStack: 1,
|
||||
},
|
||||
{
|
||||
id: "cobblestone",
|
||||
name: "Cobblestone",
|
||||
pricePerStack: 1 / 16,
|
||||
defaultStacksPerDia: 16,
|
||||
variants: ["Cobblestone", "Stone", "Stone Bricks"],
|
||||
},
|
||||
{
|
||||
id: "sand",
|
||||
name: "Sand",
|
||||
pricePerStack: 1,
|
||||
},
|
||||
{
|
||||
id: "gunpowder",
|
||||
name: "Gunpowder",
|
||||
pricePerStack: 1 / 4,
|
||||
defaultStacksPerDia: 4,
|
||||
},
|
||||
];
|
||||
16
src/utils/formatDiamonds.ts
Normal file
16
src/utils/formatDiamonds.ts
Normal file
@ -0,0 +1,16 @@
|
||||
export function formatDiamonds(totalDia: number): string {
|
||||
const roundedDia = Math.round(totalDia);
|
||||
|
||||
if (roundedDia < 64) {
|
||||
return `${roundedDia} Dia`;
|
||||
}
|
||||
|
||||
const stacks = Math.floor(roundedDia / 64);
|
||||
const remaining = roundedDia % 64;
|
||||
|
||||
if (remaining === 0) {
|
||||
return `${stacks} ${stacks === 1 ? "stack" : "stacks"} Dia`;
|
||||
}
|
||||
|
||||
return `${stacks} ${stacks === 1 ? "stack" : "stacks"} + ${remaining} Dia`;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user