Skip to content
This repository was archived by the owner on Jun 2, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions basic-search-frontend/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": ["next/core-web-vitals", "next/typescript"]
}
36 changes: 36 additions & 0 deletions basic-search-frontend/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env*.local

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
23 changes: 23 additions & 0 deletions basic-search-frontend/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
## Running Locally

To run this project locally, start by setting up your environment variables. Copy the definitions from [`.env.example`](https://github.com/hypermodeAI/hyper-commerce-frontend/blob/main/.env.example) into a new file named `.env.local` at the root of your project, and provide the values from your Hypermode dashboard.

Once your environment variables are configured, install the necessary dependencies with:

```
npm install
```

Then, start the development server using:

```
npm run dev
```

Your app should be up and running at [localhost:3000](http://localhost:3000/)

## Deploy with Vercel

[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/import/project?template=https://github.com/hypermodeAI/hyper-commerce-frontend)

_NOTES_: Make sure your environment variables are added to your Vercel project.
75 changes: 75 additions & 0 deletions basic-search-frontend/app/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"use server";

type FetchQueryProps = {
query: string;
variables?: { [key: string]: unknown };
};

const fetchQuery = async ({ query, variables }: FetchQueryProps) => {
try {
const res = await fetch(process.env.HYPERMODE_API_ENDPOINT as string, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.HYPERMODE_API_TOKEN}`,
},
body: JSON.stringify({
query,
variables,
}),
cache: "no-store",
});

if (res.status < 200 || res.status >= 300) {
throw new Error(res.statusText);
}

const { data, error, errors } = await res.json();
return { data, error: error || errors };
} catch (err) {
console.error("error in fetchQuery:", err);
return { data: null, error: err };
}
};

export async function searchProducts(
query: string,
maxItems: number,
thresholdStars: number,
inStockOnly: boolean = false
) {
const graphqlQuery = `
query searchProducts($query: String!, $maxItems: Int!, $thresholdStars: Float!, $inStockOnly: Boolean!) {
searchProducts(query: $query, maxItems: $maxItems, thresholdStars: $thresholdStars, inStockOnly: $inStockOnly) {
searchObjs {
product {
name
id
image
description
stars
price
isStocked
category
}
}
}
}
`;

const { error, data } = await fetchQuery({
query: graphqlQuery,
variables: {
query,
maxItems,
thresholdStars,
inStockOnly,
},
});

if (error) {
return { error: Array.isArray(error) ? error[0] : error };
} else {
return { data };
}
}
Binary file added basic-search-frontend/app/favicon.ico
Binary file not shown.
3 changes: 3 additions & 0 deletions basic-search-frontend/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
36 changes: 36 additions & 0 deletions basic-search-frontend/app/header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import Search from "./search";
import { Suspense } from "react";
import Logo from "./logo";

export default function Header() {
return (
<>
<div className="md:flex hidden items-center justify-between space-x-4 px-4">
<Suspense fallback={<div>Loading</div>}>
<Logo />
</Suspense>
<Suspense fallback={<SearchSkeleton />}>
<Search />
</Suspense>
</div>
<div className="md:hidden items-center justify-between px-4">
<div className="flex items-center justify-between mb-2">
<Suspense fallback={<div>Loading</div>}>
<Logo />
</Suspense>
</div>
<Suspense fallback={<SearchSkeleton />}>
<Search />
</Suspense>
</div>
</>
);
}

function SearchSkeleton() {
return (
<div className="relative flex flex-1 flex-shrink-0">
<div className="peer block w-full h-11 rounded-md border border-stone-700 py-[9px] pl-14 text-sm outline-2 placeholder:text-gray-500 bg-black" />
</div>
);
}
23 changes: 23 additions & 0 deletions basic-search-frontend/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { Metadata } from "next";
import "./globals.css";
import Header from "./header";

export const metadata: Metadata = {
title: "Hypermode Search",
description: "A simple template with real-time search using Hypermode.",
};

export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className="max-w-6xl mx-auto bg-stone-900 text-white my-8 space-y-8">
<Header />
<div>{children}</div>
</body>
</html>
);
}
32 changes: 32 additions & 0 deletions basic-search-frontend/app/logo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"use client";

import Link from "next/link";

export default function Logo() {
return (
<Link href="https://github.com/hypermodeAI/hyper-commerce?tab=readme-ov-file#hyper-commerce">
<div className="text-white flex items-center font-semibold text-xl italic">
<svg
xmlns="http://www.w3.org/2000/svg"
width="26"
height="26"
fill="none"
className="fill-foreground"
viewBox="0 0 130 124"
>
<g clipPath="url(#a)">
<path
fill="#fff"
d="M90.2 0 76 55.5H39.5L53.7 0H14.2L0 55.5h32.3l-16.7 65.8 60.3-65.4-17.2 67.2h39.5L129.7 0z"
/>
</g>
<defs>
<clipPath id="a">
<path fill="#fff" d="M0 0h130v123.2H0z" />
</clipPath>
</defs>
</svg>
</div>
</Link>
);
}
68 changes: 68 additions & 0 deletions basic-search-frontend/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { Suspense } from "react";
import { searchProducts } from "./actions";

export const metadata = {
title: "Search",
description: "Search for products in the store.",
};

export default async function SearchPage({
searchParams,
}: {
searchParams?: { [key: string]: string | string[] | undefined };
}) {
const { q: searchValue } = searchParams as {
[key: string]: string;
};

const response = await searchProducts(searchValue, 20, 0);

const products = response?.data?.searchProducts?.searchObjs || [];

return (
<div className="px-4 flex md:flex-row flex-col md:space-x-4 space-y-4 md:space-y-0 w-full">
<div className="w-full">
<Suspense fallback={<div>Loading...</div>}>
<div>
{products?.length > 0 ? (
<div className="">
{products.map(
(
item: {
product: {
description: string;
name: string;
};
},
i: number
) => (
<div key={i} className="mb-8">
<div>
<div className="font-semibold text-xl mb-1">
{item.product.name}
</div>
<div className="text-stone-400">
{item.product.description}
</div>
</div>
</div>
)
)}
</div>
) : (
<div className="py-4 text-white/40 flex flex-col items-center justify-center w-full">
<div className="font-semibold text-2xl">
No items matching your search
</div>
<div>
Try searching again, such as: &quot;Halloween
decorations&quot;
</div>
</div>
)}
</div>
</Suspense>
</div>
</div>
);
}
39 changes: 39 additions & 0 deletions basic-search-frontend/app/search.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"use client";

import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
import { useSearchParams, usePathname, useRouter } from "next/navigation";
import { useDebouncedCallback } from "use-debounce";

export default function Search() {
const searchParams = useSearchParams();
const pathname = usePathname();
const { replace } = useRouter();

const handleSearch = useDebouncedCallback((term: string) => {
const params = new URLSearchParams(searchParams);

if (term) {
params.set("q", term);
} else {
params.delete("q");
}
replace(`${pathname}?${params.toString()}`);
}, 200);

return (
<div className="relative flex flex-1 flex-shrink-0">
<label htmlFor="search" className="sr-only">
Search
</label>
<input
className="border-stone-700 peer block w-full rounded-md border py-[9px] pl-14 text-sm outline-2 placeholder:text-gray-500 bg-black"
placeholder="Search"
defaultValue={searchParams?.get("q") || ""}
onChange={(e) => {
handleSearch(e.target.value);
}}
/>
<MagnifyingGlassIcon className="absolute left-6 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500 peer-focus:text-gray-900" />
</div>
);
}
4 changes: 4 additions & 0 deletions basic-search-frontend/next.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};

export default nextConfig;
Loading