diff --git a/app/items-results.tsx b/app/items-results.tsx
new file mode 100644
index 0000000..def272b
--- /dev/null
+++ b/app/items-results.tsx
@@ -0,0 +1,104 @@
+"use client";
+
+import SearchButton from "@/app/search";
+import { t, type Locale } from "@/lib/i18n";
+import type { Item } from "@/lib/items";
+import { useState } from "react";
+
+export default function ItemsResults({
+ initialItems,
+ initialError,
+ locale,
+}: {
+ initialItems: Item[];
+ initialError: string;
+ locale: Locale;
+}) {
+ const [items, setItems] = useState(initialItems);
+ const [error, setError] = useState(initialError);
+
+ function handleResults(nextItems: Item[], nextError: string) {
+ setItems(nextItems);
+ setError(nextError);
+ }
+
+ return (
+ <>
+
+
+
+
{t(locale, "articles.count")}
+
{items.length}
+
+
+
+
+
+
+
+ {t(locale, "articles.heading")}
+
+
+
+
+
+
+
+ {error && (
+
+ {t(locale, "articles.error")}
+
+ )}
+
+ {items.length > 0 && (
+
+
+
+
+
+ {t(locale, "articles.code")}
+
+
+ {t(locale, "articles.descriptionColumn")}
+
+
+
+
+ {items.map((item) => (
+
+
+ {item.ItemCode}
+
+ {item.ItemName}
+
+ ))}
+
+
+
+ )}
+
+ {items.length === 0 && !error && (
+
+
+ π¦
+
+
+ {t(locale, "articles.emptyTitle")}
+
+
+ {t(locale, "articles.emptyDescription")}
+
+
+ )}
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/app/page.tsx b/app/page.tsx
index 0fab664..bebe448 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,5 +1,6 @@
import LogoutButton from "@/app/logout-button";
import LanguageSwitcher from "@/app/language-switcher";
+import ItemsResults from "@/app/items-results";
import { getItems } from "@/lib/items";
import { cookies } from "next/headers";
import { getLocale, LOCALE_COOKIE, t } from "@/lib/i18n";
@@ -31,85 +32,7 @@ export default async function ItemsPage() {
- {/* KPI Card */}
-
-
-
-
{t(locale, "articles.count")}
-
- {items.length}
-
-
-
-
-
- {/* Main Card */}
-
-
-
- {t(locale, "articles.heading")}
-
-
-
- {/* Error */}
- {error && (
-
- {t(locale, "articles.error")}
-
- )}
-
- {/* Table */}
- {items.length > 0 && (
-
-
-
-
-
- {t(locale, "articles.code")}
-
-
- {t(locale, "articles.descriptionColumn")}
-
-
-
-
-
- {items.map((item) => (
-
-
- {item.ItemCode}
-
-
-
- {item.ItemName}
-
-
- ))}
-
-
-
- )}
-
- {/* Empty State */}
- {items.length === 0 && !error && (
-
-
- π¦
-
-
-
- {t(locale, "articles.emptyTitle")}
-
-
-
- {t(locale, "articles.emptyDescription")}
-
-
- )}
-
+
);
diff --git a/app/search.tsx b/app/search.tsx
new file mode 100644
index 0000000..286c604
--- /dev/null
+++ b/app/search.tsx
@@ -0,0 +1,42 @@
+"use client";
+
+import { useState } from "react";
+import { t, type Locale } from "@/lib/i18n";
+import { searchItems, type Item } from "@/lib/items";
+
+export default function SearchButton({
+ locale,
+ onResults,
+}: {
+ locale: Locale;
+ onResults: (items: Item[], error: string) => void;
+}) {
+ const [loading, setLoading] = useState(false);
+
+ async function handleSearch() {
+ const query = (document.getElementById("search-input") as HTMLInputElement)?.value.trim();
+ if (!query) return;
+
+ setLoading(true);
+
+ try {
+ const result = await searchItems(query);
+ onResults(result.items, result.error);
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ return (
+ <>
+
+
+ {loading ? t(locale, "articles.loading") : t(locale, "articles.search")}
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/lib/i18n.ts b/lib/i18n.ts
index 5082df0..31e6798 100644
--- a/lib/i18n.ts
+++ b/lib/i18n.ts
@@ -15,6 +15,7 @@ type TranslationKey =
| "articles.emptyTitle"
| "articles.emptyDescription"
| "articles.error"
+ | "articles.search"
| "auth.logout"
| "login.title"
| "login.description"
@@ -40,6 +41,7 @@ const translations: Record> = {
"articles.emptyTitle": "Geen artikelen gevonden",
"articles.emptyDescription": "De Service Layer heeft geen artikelen teruggegeven.",
"articles.error": "Er is een fout opgetreden bij het ophalen van de artikelen.",
+ "articles.search": "Zoeken",
"auth.logout": "Uitloggen",
"login.title": "Inloggen",
"login.description": "Log in om de artikelen te bekijken.",
@@ -64,6 +66,7 @@ const translations: Record> = {
"articles.emptyTitle": "No items found",
"articles.emptyDescription": "The Service Layer returned no items.",
"articles.error": "An error occurred while retrieving the items.",
+ "articles.search": "Search",
"auth.logout": "Log out",
"login.title": "Sign in",
"login.description": "Sign in to view the items.",
@@ -88,6 +91,7 @@ const translations: Record> = {
"articles.emptyTitle": "ζ΅εε°θ²¨ε",
"articles.emptyDescription": "Service Layer εθΏεδ»»δ½θ²¨εγ",
"articles.error": "εεΎθ²¨εζηΌηι―θͺ€γ",
+ "articles.search": "ζε°",
"auth.logout": "η»εΊ",
"login.title": "η»ε
₯",
"login.description": "η»ε
₯εΎζ₯η貨εγ",
diff --git a/lib/items.ts b/lib/items.ts
index e5291cf..46f3018 100644
--- a/lib/items.ts
+++ b/lib/items.ts
@@ -1,7 +1,9 @@
+"use server";
+
import { executeHttpRequest } from "@sap-cloud-sdk/http-client";
-type Item = {
+export type Item = {
ItemCode: string;
ItemName: string;
};
@@ -22,3 +24,22 @@ export async function getItems(): Promise<{ items: Item[]; error: string }> {
};
}
}
+
+export async function searchItems(query: string): Promise<{ items: Item[]; error: string }> {
+ const escapedQuery = query.replaceAll("'", "''");
+
+ try {
+ const response = await executeHttpRequest(
+ { destinationName: "SAP4SL" },
+ { method: "GET", url: `/b1s/v2/Items?$filter=contains(ItemName,'${escapedQuery}')` },
+ );
+
+ return { items: response.data.value || [], error: "" };
+ } catch (error) {
+ console.error(error);
+ return {
+ items: [],
+ error: "items_request_failed",
+ };
+ }
+}