Skip to content
Open
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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@ Here are some useful links for getting started with Wix Headless and the availab
<td></td>
<td>Wix Rentals</td>
</tr>
<tr>
<td><a href="https://github.com/wix/headless-templates/tree/main/astro/benny-pets">Benny Pets</a></td>
<td>A designed storefront with variant selection, cart, and checkout using Astro and Wix Headless.</td>
<td><a href="https://benny-site-tuvitk-0a06.wix-site-host.com">Live demo</a></td>
<td>Wix Stores, Wix eCommerce, Wix Forms</td>
</tr>
<tr>
<td colspan="4" align="center"><strong>Next.js</strong></td>
</tr>
Expand Down
28 changes: 28 additions & 0 deletions astro/benny-pets/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# wix integration
.wix/

# build output
dist/

# generated types
.astro/

# dependencies
node_modules/

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

# environment variables
.env
.env.production
.env.local

# macOS-specific files
.DS_Store

# jetbrains setting folder
.idea/
4 changes: 4 additions & 0 deletions astro/benny-pets/.vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"recommendations": ["astro-build.astro-vscode"],
"unwantedRecommendations": []
}
11 changes: 11 additions & 0 deletions astro/benny-pets/.vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"command": "./node_modules/.bin/astro dev",
"name": "Development server",
"request": "launch",
"type": "node-terminal"
}
]
}
5 changes: 5 additions & 0 deletions astro/benny-pets/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"editor.quickSuggestions": {
"strings": "on"
}
}
53 changes: 53 additions & 0 deletions astro/benny-pets/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Benny Pets — Wix Stores storefront

A designed storefront on top of [Wix Stores](https://dev.wix.com/docs/sdk/backend-modules/stores/introduction)
and [Wix eCommerce](https://dev.wix.com/docs/sdk/backend-modules/ecom/introduction): a catalog,
a product page with variant selection, a cart, and checkout — plus a contact
form rendered from [Wix Forms](https://dev.wix.com/docs/sdk/backend-modules/forms/introduction).

Everything on screen comes from Wix at request time. Every route is
server-rendered, and the page ships no framework runtime — the animation and
cart interactions are plain modules, so there are no islands to hydrate.

## Wix apps used

| App | What it powers |
|---|---|
| Wix Stores | The catalog: products, prices, images, options and variants |
| Wix eCommerce | The visitor's cart and the checkout session |
| Wix Forms | The footer contact form — fields come from the form definition |

## Routes

| Route | What it does |
|---|---|
| `/` | Home — best sellers, a catalog-driven gallery, and the contact form |
| `/store` | The full catalog |
| `/store/[slug]` | Product detail with option selection; unknown slugs 404 |
| `/cart` | The visitor's cart, with quantity, removal, and checkout |
| `POST /api/cart/add` | `{ productId, variantId?, quantity }` — refuses to guess a variant |
| `POST /api/cart/update` | `{ lineItemId, quantity }` |
| `POST /api/cart/remove` | `{ lineItemId }` |
| `POST /api/checkout` | Creates a checkout and returns its redirect-session URL |
| `POST /api/contact` | Submits the contact form, keyed by each field's Wix target |

## Getting started

```bash
npm install
npm run dev
```

The `@wix/astro` integration authenticates every SDK call from the visitor's
session, so there is no client to construct and no client id in the source.

## Notes

- **Prices** are the `formattedAmount` Wix returns, so the store's own currency
is always what shows.
- **Variants**: a product with options can't be added until one is chosen, and
the price updates to the chosen variant. Single-variant products get a
one-click add.
- **The contact form's id** lives in `src/lib/constants.ts`. It matches the form
this template's site provisions; point it at your own form if you connect
this code to a site you built yourself.
40 changes: 40 additions & 0 deletions astro/benny-pets/astro.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// @ts-check
import { defineConfig } from "astro/config";
import wix from "@wix/astro";
import wixPages from "@wix/astro-pages";
import wixHostingAdapter from "@wix/astro-wix-hosting-adapter";
import { transform } from "esbuild";

// Astro ships the SSR build unminified and `vite.build.minify` only reaches the client build. The
// deploy transport picks its path by server-bundle size, and past ~2.25MiB of server code it
// switches to a route that cannot authorize — so this is what keeps the template provisionable.
const minifyServerBundle = () => {
let isSsr = false;
return {
name: "benny-pets:minify-server-bundle",
apply: "build",
enforce: "post",
configResolved(config) {
isSsr = Boolean(config.build.ssr);
},
async renderChunk(code) {
if (!isSsr) return null;
const { code: minified } = await transform(code, {
minify: true, format: "esm", target: "node20", keepNames: true, legalComments: "none",
});
return { code: minified, map: null };
},
};
};

// https://astro.build/config
export default defineConfig({
// Every route renders per request: the catalog, the cart and the contact
// form are all read from Wix at request time.
output: "server",
adapter: wixHostingAdapter(),
integrations: [wix(), wixPages()],
security: { checkOrigin: false },

vite: { plugins: [minifyServerBundle()] },
});
29 changes: 29 additions & 0 deletions astro/benny-pets/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "wix-astro-benny-pets",
"type": "module",
"version": "0.0.1",
"scripts": {
"astro": "astro",
"dev": "astro dev",
"build": "astro build"
},
"dependencies": {
"@wix/astro": "^2.39.0",
"@wix/astro-pages": "^2.0.4",
"@wix/astro-wix-hosting-adapter": "^2.0.0",
"@wix/categories": "^1.0.216",
"@wix/ecom": "^1.0.2252",
"@wix/essentials": "^1.0.6",
"@wix/forms": "^1.0.462",
"@wix/redirects": "^1.0.118",
"@wix/sdk": "^1.21.5",
"@wix/stores": "^1.0.829",
"astro": "^5.8.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"typescript": "^5.8.3"
},
"devDependencies": {
"esbuild": "^0.27.7"
}
}
Binary file added astro/benny-pets/public/favicon.ico
Binary file not shown.
Binary file added astro/benny-pets/public/gallery1.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added astro/benny-pets/public/gallery2.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added astro/benny-pets/public/gallery3.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added astro/benny-pets/public/hero-bg.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
103 changes: 103 additions & 0 deletions astro/benny-pets/src/components/ContactForm.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
---
import { getContactFields, type ContactField } from "../lib/contact-form";

// The fields come from Wix Forms, so relabelling or adding a field in the
// dashboard changes this form with no code change.
const fields: ContactField[] = await getContactFields();
---

{
fields.length > 0 && (
<form class="footer-form animate-in from-right" id="contact-form">
{fields.map((field) =>
field.type === "textarea" ? (
<textarea
class="footer-textarea"
name={field.target}
data-type={field.type}
placeholder={field.label}
required={field.required}
/>
) : field.type === "select" ? (
<select class="footer-input" name={field.target} data-type={field.type} required={field.required}>
<option value="">{field.label}</option>
{field.options.map((option) => (
<option value={option}>{option}</option>
))}
</select>
) : field.type === "checkbox" ? (
<label class="footer-checkbox">
<input type="checkbox" name={field.target} data-type={field.type} required={field.required} />
<span>{field.label}</span>
</label>
) : (
<input
class="footer-input"
type={field.type}
name={field.target}
data-type={field.type}
placeholder={field.label}
required={field.required}
/>
),
)}
<div class="footer-send-row">
<button type="submit" class="footer-send-btn">
Send
</button>
</div>
</form>
)
}

<script>
import { showToast } from "../scripts/site.js";

const form = document.getElementById("contact-form");

// Values are typed per the field's own definition — a number field submits a
// number, a checkbox a boolean, a checkbox group an array.
function readValues(formEl) {
const data = new FormData(formEl);
const values = {};
formEl.querySelectorAll("[name][data-type]").forEach((input) => {
const name = input.name;
const type = input.dataset.type;
if (type === "checkbox") {
values[name] = input.checked;
} else if (type === "checkbox-group") {
values[name] = data.getAll(name);
} else if (type === "number") {
const raw = data.get(name);
values[name] = raw === "" || raw === null ? null : Number(raw);
} else {
values[name] = data.get(name) ?? "";
}
});
return values;
}

form?.addEventListener("submit", async (event) => {
event.preventDefault();
const button = form.querySelector("button[type=submit]");
if (button.disabled) return;
button.disabled = true;
button.textContent = "…";
try {
const res = await fetch("/api/contact", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(readValues(form)),
});
if (!res.ok) throw new Error("submission rejected");
showToast("Message sent!");
form.reset();
} catch (err) {
console.error("[contact] submission failed:", err);
showToast("Could not send — please try again.");
} finally {
button.disabled = false;
button.textContent = "Send";
}
});
</script>
24 changes: 24 additions & 0 deletions astro/benny-pets/src/components/Nav.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
import WaveLink from "./WaveLink.astro";

interface Props {
cartCount?: number;
/** The homepage calls it "Shop", the store and cart pages call it "Store". */
shopLabel?: string;
/** On the homepage, "Home" scrolls to the top instead of reloading. */
homeScrollsToTop?: boolean;
}

const { cartCount = 0, shopLabel = "Store", homeScrollsToTop = false } = Astro.props;
---

<ul class="hero-nav">
<li>
<WaveLink href="/" text="Home" id={homeScrollsToTop ? "nav-home" : undefined} />
</li>
<li><WaveLink href="/store" text={shopLabel} /></li>
<li><WaveLink href="/#about" text="About" /></li>
<li>
<WaveLink href="/cart" text={cartCount > 0 ? `Cart (${cartCount})` : "Cart"} cartLabel />
</li>
</ul>
31 changes: 31 additions & 0 deletions astro/benny-pets/src/components/ProductCard.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
import type { Product } from "../lib/catalog";

interface Props {
product: Product;
delay?: number;
}

const { product, delay = 0 } = Astro.props;
---

<a
href={`/store/${product.slug}`}
class="product-card animate-in"
data-categories={product.categoryIds.join(" ")}
style={`transition-delay: ${delay}s; text-decoration: none;`}
>
{
product.imageUrl ? (
<img class="product-thumb" src={product.imageUrl} alt={product.name} />
) : (
<div class="product-thumb product-thumb-placeholder" aria-label={product.name} />
)
}
<div class="product-info">
<p class="product-name">{product.name}</p>
{product.sub && <p class="product-sub">{product.sub}</p>}
<p class="product-price">{product.price}</p>
<span class="add-to-cart">View Product</span>
</div>
</a>
25 changes: 25 additions & 0 deletions astro/benny-pets/src/components/QuickAdd.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
import type { Product } from "../lib/catalog";

interface Props {
product: Product;
class?: string;
}

const { product, class: className } = Astro.props;
// A product with more than one variant needs the shopper to choose first, so
// it links to the detail page instead of adding something on their behalf.
const needsChoice = product.variantCount > 1;
---

{
needsChoice ? (
<a href={`/store/${product.slug}`} class={className}>
Choose Options
</a>
) : (
<button class={className} data-quick-add data-product-id={product._id} data-name={product.name}>
Add to Cart
</button>
)
}
Loading