Fully functional
This commit is contained in:
@@ -19,8 +19,20 @@ const addToCart = async (barcode: number) => {
|
||||
'GROCY-API-KEY': "VCPlNborGO2t8rs08cvqodalf3AjecRwgexWzkIk221mtshLoM"
|
||||
}
|
||||
});
|
||||
|
||||
const userfields = await request2.json();
|
||||
console.log(`Price of product ${data.product.name} is ${userfields.kuelschrankpreis} EUR`);
|
||||
return {name: data.product.name, price: userfields.kuelschrankpreis};
|
||||
|
||||
if ( data.product.picture_file_name ) {
|
||||
const image_fetch = await fetch(`https://development.vonhelmersen.online/api/files/productpictures/${btoa(data.product.picture_file_name)}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'accept': "application/octet-stream",
|
||||
'GROCY-API-KEY': "VCPlNborGO2t8rs08cvqodalf3AjecRwgexWzkIk221mtshLoM"
|
||||
}});
|
||||
const img_blob = await image_fetch.blob()
|
||||
return {name: data.product.name, price: userfields.kuelschrankpreis, img_blob: img_blob};
|
||||
} else {
|
||||
return {name: data.product.name, price: userfields.kuelschrankpreis};
|
||||
}
|
||||
}
|
||||
export default addToCart;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use server"
|
||||
|
||||
const API_KEY = "sup_sk_t6Jf0FwYUAkbjxzvbs6e3BkIePxm0OR3m"
|
||||
const HOSTNAME = "kasse.fet.at"
|
||||
|
||||
import SumUp from "@sumup/sdk";
|
||||
|
||||
@@ -13,10 +14,12 @@ const checkout = async (total: number) => {
|
||||
const merchantCode = (await client.merchant.getMerchantProfile()).merchant_code || "";
|
||||
const readerId = (await client.readers.list(merchantCode)).items[0].id || "";
|
||||
|
||||
const respomse = await client.readers.createCheckout(merchantCode, readerId, {total_amount: {value: total * 100, currency: "EUR", minor_unit: 2}});
|
||||
const response = await client.readers.createCheckout(merchantCode, readerId, {
|
||||
total_amount: {value: total * 100, currency: "EUR", minor_unit: 2},
|
||||
return_url: `https://${HOSTNAME}/order-status`
|
||||
});
|
||||
await new Promise(resolve => setTimeout(resolve, 10000));
|
||||
const checkout = await client.transactions.get(merchantCode, {client_transaction_id: respomse.data?.client_transaction_id || ""});
|
||||
console.log("Checkout status:", checkout);
|
||||
return response.data?.client_transaction_id || "";
|
||||
}
|
||||
|
||||
export default checkout;
|
||||
|
||||
57
src/app/api/order-status/route.ts
Normal file
57
src/app/api/order-status/route.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
// app/api/order-status/[id]/route.ts
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { EventEmitter } from "stream";
|
||||
|
||||
export const orderEvents = new EventEmitter();
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
// const orderId = params.id;
|
||||
const orderId = req.nextUrl.searchParams.get("id");
|
||||
|
||||
return new Response(
|
||||
new ReadableStream({
|
||||
start(controller) {
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
const listener = (data: { id: string; event_type: string }) => {
|
||||
// TODO!!!! UNCOMMEND THIS
|
||||
// if (data.orderId != orderId) {
|
||||
// return;
|
||||
// }
|
||||
console.error("THIS SHOULD BE UNCOMMENTED");
|
||||
console.log("Sending event:", data);
|
||||
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`));
|
||||
};
|
||||
|
||||
// orderEvents.on("update", listener);
|
||||
orderEvents.addListener("update", listener);
|
||||
|
||||
// cleanup when client disconnects
|
||||
req.signal.addEventListener("abort", () => {
|
||||
orderEvents.off("update", listener);
|
||||
controller.close();
|
||||
});
|
||||
},
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
Connection: "keep-alive",
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
|
||||
const body = await req.json();
|
||||
console.log("Webhook received:", body);
|
||||
const event_type = body.event_type || "";
|
||||
const id = body.id || "";
|
||||
|
||||
orderEvents.emit("update", { id, event_type });
|
||||
|
||||
return NextResponse.json({ message: 'Webhook received' });
|
||||
}
|
||||
@@ -1,26 +1,11 @@
|
||||
@import "tailwindcss";
|
||||
@plugin "flyonui";
|
||||
@import "flyonui/variants.css";
|
||||
@plugin "@iconify/tailwind4";
|
||||
@source "./node_modules/flyonui/flyonui.js";
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import { Geist, Geist_Mono, Inter_Tight } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import FlyonuiScript from '../components/FlyonuiScript';
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
@@ -24,11 +25,15 @@ export default function RootLayout({
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
{children}
|
||||
</body>
|
||||
<body>
|
||||
<div className="p-6">
|
||||
<div className={`flex w-full flex-row justify-center items-center mb-4 bg-primary h-16 rounded-2xl`}>
|
||||
<h1 className="text-3xl text-white">Baroness</h1>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
<FlyonuiScript />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
218
src/app/page.tsx
218
src/app/page.tsx
@@ -1,10 +1,20 @@
|
||||
"use client"
|
||||
import addToCart from "@/actions/add-to-cart";
|
||||
import checkout from "@/actions/checkout";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const createTableImage = (img_blob: Blob | undefined) => {
|
||||
if (!img_blob) {
|
||||
return <div className="flex h-full w-full items-center justify-center text-3xl">?</div>
|
||||
} else {
|
||||
return <img src={URL.createObjectURL(img_blob || "")} alt="product image" />
|
||||
}
|
||||
}
|
||||
|
||||
const quick_products = [ {name: "Cola"}, {name: "Fanta"}, {name: "Wasser"}, {name: "Red Bull"}, {name: "Bier"}, {name: "Wein"} ];
|
||||
|
||||
export default function Home() {
|
||||
const [ cart, setCart ] = useState<{name: string, price: number}[]>([]);
|
||||
const [ cart, setCart ] = useState<{name: string, price: number, img_blob?: Blob | undefined}[]>([]);
|
||||
const handleSubmit = async (formData: FormData) => {
|
||||
const barcode = formData.get("barcode")? Number(formData.get("barcode")) : 0;
|
||||
addToCart(barcode).then((item) => {
|
||||
@@ -17,25 +27,197 @@ export default function Home() {
|
||||
cart.forEach((item) => {
|
||||
total += (Number(item.price));
|
||||
});
|
||||
checkout(total);
|
||||
setCart([]);
|
||||
const client_checkout_id = checkout(total);
|
||||
setStatus("Initializing");
|
||||
window.HSOverlay.open('#transparent-modal');
|
||||
}
|
||||
|
||||
const [status, setStatus] = useState("NONE");
|
||||
|
||||
useEffect(() => {
|
||||
const but = document.getElementById("open-modal");
|
||||
but?.addEventListener("click", () => {
|
||||
window.HSOverlay.open('#failed-modal');
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const es = new EventSource(`/api/order-status/`);
|
||||
console.log("EventSource created");
|
||||
|
||||
es.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
setStatus(data.event_type);
|
||||
console.log("Received event:", data);
|
||||
|
||||
switch(data.event_type) {
|
||||
case "PENDING":
|
||||
setStatus("Verarbeiteung...");
|
||||
break;
|
||||
case "PAID":
|
||||
// Order is completed
|
||||
setStatus("Erfolgreich bezahlt!");
|
||||
window.HSOverlay.close('#transparent-modal')
|
||||
window.HSOverlay.open('#success-modal');
|
||||
setStatus("NONE");
|
||||
setCart([]);
|
||||
break;
|
||||
case "FAILED":
|
||||
window.HSOverlay.open('#failed-modal');
|
||||
setStatus("NONE");
|
||||
break;
|
||||
default:
|
||||
// Unknown event type
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
return () => es.close();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1>Barcode:</h1>
|
||||
<form action={handleSubmit}>
|
||||
<input type="number" name="barcode" placeholder="Enter barcode" />
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
<h2>Cart:</h2>
|
||||
<ul>
|
||||
{cart.map((item, index) => (
|
||||
<li key={index}>{item.name}: {item.price} EUR</li>
|
||||
))}
|
||||
</ul>
|
||||
<form action={handleCheckout}>
|
||||
<button type="submit">Checkout</button>
|
||||
</form>
|
||||
<div className="flex flex-row px-6 gap-6">
|
||||
<div className="w-3/5">
|
||||
<div className="border-base-content/25 w-full rounded-lg border">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><span className="icon-[tabler--barcode] size-16"></span></td>
|
||||
<td>Scanne den Barcode deines Produkts um es dem Warenkorb hinzuzufügen.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span className="icon-[tabler--shopping-cart] size-16"></span></td>
|
||||
<td><span>Überprüfe deinen Warenkorb und entferne Produkte, die du nicht kaufen möchtest.</span><br /><span> Klicke anschließend auf Bezahlen.</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span className="icon-[tabler--credit-card] size-16"></span></td>
|
||||
<td>Bezahlen mit Karte am Karten Terminal</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<h2 className="mt-6 text-xl">Schnellauswahl:</h2>
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
{quick_products.map((item, index) => (
|
||||
<button key={index} className="btn btn-primary" onClick={async () => {
|
||||
const new_item = await addToCart(item.name === "Bier" ? 400014 : item.name === "Wein" ? 400015 : item.name === "Cola" ? 400000 : item.name === "Fanta" ? 400001 : item.name === "Wasser" ? 400002 : item.name === "Red Bull" ? 400003 : 0);
|
||||
const newCart = [...cart, new_item];
|
||||
setCart(newCart);
|
||||
}}>{item.name}</button>
|
||||
))}
|
||||
</div>
|
||||
<h2 className="text-xl">Barcode:</h2>
|
||||
<form action={handleSubmit}>
|
||||
<div className="input max-w-sm" data-input-number>
|
||||
<input type="text" name="barcode" aria-label="Input barcode" data-input-text-input />
|
||||
<span className="my-auto flex gap-3">
|
||||
<button type="button" className="btn btn-primary btn-soft size-5.5 min-h-0 rounded-sm p-0" aria-label="Decrement button" data-input-number-decrement >
|
||||
<span className="icon-[tabler--minus] size-3.5 shrink-0"></span>
|
||||
</button>
|
||||
<button type="button" className="btn btn-primary btn-soft size-5.5 min-h-0 rounded-sm p-0" aria-label="Increment button" data-input-number-increment >
|
||||
<span className="icon-[tabler--plus] size-3.5 shrink-0"></span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<button className="btn btn-primary" type="submit">Submit</button>
|
||||
</form>
|
||||
<button id="open-modal" className="btn btn-primary">Show Modal</button>
|
||||
</div>
|
||||
<div className="w-2/5">
|
||||
<h2>Cart:</h2>
|
||||
<table className="table-borderless table-striped table ">
|
||||
<thead>
|
||||
<tr className="border-0 bg-base-300/20 *:first:rounded-s-md *:last:rounded-e-md">
|
||||
<th>Bild</th>
|
||||
<th>Name</th>
|
||||
<th>Preis</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{cart.map((item, index) => (
|
||||
<tr key={index}>
|
||||
<td className="avatar">
|
||||
<div className="bg-base-content/10 h-10 w-10 rounded-md">
|
||||
{ createTableImage(item.img_blob) }
|
||||
</div>
|
||||
</td>
|
||||
<td>{item.name}</td>
|
||||
<td>{item.price}€</td>
|
||||
<td><button className="btn btn-square bg-error" onClick={
|
||||
() => {
|
||||
const newCart = cart.filter((_, i) => i !== index);
|
||||
setCart(newCart);
|
||||
}
|
||||
}><span className="icon-[tabler--trash] size-4.5 shrink-0"></span></button></td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<form action={handleCheckout}>
|
||||
<button className="btn btn-square btn-primary" aria-label="Icon Button" type="submit"><span className="icon-[tabler--shopping-cart] size-4.5 shrink-0"></span></button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="transparent-modal" className="overlay modal overlay-open:opacity-100 overlay-open:duration-300 hidden place-items-center" role="dialog" tabIndex={-1}>
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content text-white bg-primary shadow-none">
|
||||
<div className="modal-header">
|
||||
<h3 className="modal-title text-white">Bitte bezahle auf dem Karten Terminal</h3>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<div className="flex w-full flex-col justify-center items-center">
|
||||
<span className="loading loading-infinity size-56"></span>
|
||||
<span>Status: {status}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="success-modal" className="overlay modal overlay-open:opacity-100 overlay-open:duration-300 hidden place-items-center" role="dialog" tabIndex={-1}>
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content text-white bg-success shadow-none">
|
||||
<div className="modal-header">
|
||||
<h3 className="modal-title text-white">Danke für die Spende!</h3>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<div className="flex w-full flex-col justify-center items-center">
|
||||
<span className="icon-[tabler--check] size-56"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-secondary" data-overlay="#success-modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="failed-modal" className="overlay modal overlay-open:opacity-100 overlay-open:duration-300 hidden place-items-center" role="dialog" tabIndex={-1}>
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content text-white bg-red-800 shadow-none">
|
||||
<div className="modal-header">
|
||||
<h3 className="modal-title text-white">Das hat leider nicht funktioniert! Bitte versuche es erneut!</h3>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<div className="flex w-full flex-col justify-center items-center">
|
||||
<span className="icon-[tabler--poo] size-56"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-secondary" data-overlay="#failed-modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,10 +2,15 @@
|
||||
|
||||
import getReaderList from "@/actions/getReaderList";
|
||||
import registerReader from "@/actions/registerReader";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const Page = () => {
|
||||
const [ readers, setReaders ] = useState<string[]>();
|
||||
useEffect(() => {
|
||||
getReaderList().then((readerList) => {
|
||||
setReaders(readerList);
|
||||
});
|
||||
}, readers);
|
||||
const handleRegisterReader = async (formData: FormData) => {
|
||||
const readername = formData.get("readername")?.toString() || "";
|
||||
const pairingcode = formData.get("pairingcode")?.toString() || "";
|
||||
|
||||
47
src/components/FlyonuiScript.tsx
Normal file
47
src/components/FlyonuiScript.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
// FlyonuiScript.tsx
|
||||
'use client';
|
||||
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
// Optional third-party libraries
|
||||
// import $ from 'jquery';
|
||||
// import _ from 'lodash';
|
||||
// import noUiSlider from 'nouislider';
|
||||
// import 'datatables.net';
|
||||
// import 'dropzone/dist/dropzone-min.js';
|
||||
|
||||
// window.$ = $;
|
||||
// window._ = _;
|
||||
// window.jQuery = $;
|
||||
// window.DataTable = $.fn.dataTable;
|
||||
// window.noUiSlider = noUiSlider;
|
||||
|
||||
async function loadFlyonUI() {
|
||||
return import('flyonui/flyonui');
|
||||
}
|
||||
|
||||
export default function FlyonuiScript() {
|
||||
const path = usePathname();
|
||||
|
||||
useEffect(() => {
|
||||
const initFlyonUI = async () => {
|
||||
await loadFlyonUI();
|
||||
};
|
||||
|
||||
initFlyonUI();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
if (
|
||||
window.HSStaticMethods &&
|
||||
typeof window.HSStaticMethods.autoInit === 'function'
|
||||
) {
|
||||
window.HSStaticMethods.autoInit();
|
||||
}
|
||||
}, 100);
|
||||
}, [path]);
|
||||
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user