diff --git a/README.md b/README.md
index f06471a..4a84606 100644
--- a/README.md
+++ b/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.
diff --git a/src/app/api/webhook/route.ts b/src/app/api/webhook/route.ts
new file mode 100644
index 0000000..2836b7c
--- /dev/null
+++ b/src/app/api/webhook/route.ts
@@ -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 },
+ );
+ }
+}
diff --git a/src/app/globals.css b/src/app/globals.css
index f1d8c73..0f5024c 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -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;
+}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 756fcce..5a85446 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -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({
diff --git a/src/app/page.tsx b/src/app/page.tsx
index b95c3ed..ab1c8f5 100644
--- a/src/app/page.tsx
+++ b/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 (
-
- Hello world!
-
+
+
+
+
+
+
+ {steps.map((step, index) => (
+
+
+ {step}
+
+ {index < steps.length - 1 && (
+
+ )}
+
+ ))}
+
+
+
+
+ {state.currentStep === 0 &&
}
+ {state.currentStep === 1 &&
}
+ {state.currentStep === 2 &&
}
+
+
+
+
+ {state.currentStep < 2 && (
+
+ )}
+
+
+
+
+
+
+ );
+}
+
+export default function Home() {
+ return (
+
+
+
);
}
diff --git a/src/components/CartView.tsx b/src/components/CartView.tsx
new file mode 100644
index 0000000..6ad29af
--- /dev/null
+++ b/src/components/CartView.tsx
@@ -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 (
+
+
Kundvagn
+
+ {state.items.length === 0 ? (
+
Kundvagnen är tom
+ ) : (
+
+ {state.items.map((item, index) => (
+
+
+
+
+ {item.product.name}
+ {item.variant && ` (${item.variant})`}
+
+
+ Leverans:{" "}
+ {item.deliveryOption === "shulker"
+ ? "Shulker (+1 Dia)"
+ : item.deliveryOption === "pickup"
+ ? "Pickup (-10%)"
+ : "Kistor"}
+
+
+ Antal:
+
+ updateQuantity(index, parseInt(e.target.value) || 0)
+ }
+ min="0"
+ className="w-20 bg-gray-600 text-white p-1 rounded border border-gray-500"
+ />
+
+
+
+
+ {formatDiamonds(calculateItemTotal(item))}
+
+
+
+
+
+ ))}
+
+
+
+ Subtotal:
+ {formatDiamonds(subtotal)}
+
+
+ {state.discountPercentage > 0 && (
+
+ Rabatt ({state.discountCode}):
+
+ -{formatDiamonds(discount)} ({state.discountPercentage}%)
+
+
+ )}
+
+
+ Totalt:
+ {formatDiamonds(total)}
+
+
+
+
+
+
+ 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"
+ />
+
+
+
+
+ )}
+
+ );
+}
diff --git a/src/components/DeliveryDetails.tsx b/src/components/DeliveryDetails.tsx
new file mode 100644
index 0000000..1664705
--- /dev/null
+++ b/src/components/DeliveryDetails.tsx
@@ -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 (
+
+
+ Leveransuppgifter
+
+
+
+
+
+
+ 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"
+ />
+
+
+
+
+
+ !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" : ""}`}
+ />
+
+
+
+
Ordersammanfattning
+
+ {state.items.map((item, index) => (
+
+
+ {item.product.name}
+ {item.variant ? ` (${item.variant})` : ""} x{item.quantity}
+
+
+ {(() => {
+ 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));
+ })()}
+
+
+ ))}
+
+
+ Total:
+ {formatDiamonds(calculateTotal())}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/ProductSelection.tsx b/src/components/ProductSelection.tsx
new file mode 100644
index 0000000..c417695
--- /dev/null
+++ b/src/components/ProductSelection.tsx
@@ -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(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 (
+
+
+ Lägg till produkt
+
+
+
+
+
+
+
+
+ {selectedProduct?.variants && (
+
+
+
+
+ )}
+
+
+
+
+ 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"
+ />
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/contexts/CartContext.tsx b/src/contexts/CartContext.tsx
new file mode 100644
index 0000000..bcee3e2
--- /dev/null
+++ b/src/contexts/CartContext.tsx
@@ -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;
+} | null>(null);
+
+export function CartProvider({ children }: { children: ReactNode }) {
+ const [state, dispatch] = useReducer(cartReducer, initialState);
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useCart() {
+ const context = useContext(CartContext);
+ if (!context) {
+ throw new Error("useCart must be used within a CartProvider");
+ }
+ return context;
+}
diff --git a/src/data/discountCodes.json b/src/data/discountCodes.json
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/src/data/discountCodes.json
@@ -0,0 +1 @@
+{}
diff --git a/src/data/products.ts b/src/data/products.ts
new file mode 100644
index 0000000..9b793ef
--- /dev/null
+++ b/src/data/products.ts
@@ -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,
+ },
+];
diff --git a/src/types/product.ts b/src/types/product.ts
new file mode 100644
index 0000000..5a0912e
--- /dev/null
+++ b/src/types/product.ts
@@ -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,
+ },
+];
diff --git a/src/utils/formatDiamonds.ts b/src/utils/formatDiamonds.ts
new file mode 100644
index 0000000..510205c
--- /dev/null
+++ b/src/utils/formatDiamonds.ts
@@ -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`;
+}