Skip to content
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
50 changes: 40 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,51 @@
# Commerce

> Commerce is a Statamic addon starter.
Statamic commerce addon with product sync, cart/checkout tags, Stripe webhooks, and optional shop scaffolding.

## Features
## Install

Planned features:
```bash
composer require daugtcom/statamic-commerce
```

- TBD
## Commands

## How to Install
### Install collections/blueprints/taxonomies

You can install this addon via Composer:
```bash
php please daugt-commerce:install
```

``` bash
composer require daugtcom/statamic-commerce
### Make optional shop (listing/detail/search/filter/cart)

```bash
php please daugt-commerce:make-shop
```

## How to Use
This creates:
- `resources/views/shop/index.antlers.html`
- `resources/views/shop/_cart.antlers.html`
- `resources/views/products/product.antlers.html`
- `resources/views/checkout.antlers.html`
- optional `pages/shop` entry using template `shop/index` (unless `--without-shop-page`)
- optional `pages/checkout` entry using template `checkout` (unless `--without-checkout-page`)

Add `{{ partial:shop/cart }}` once in your main layout to mount the cart drawer globally.
Shop templates are copied from addon source views in `resources/views/...` (shop index, cart partial, product detail).

Use `--force` to overwrite existing shop files or to re-ensure the generated shop page entries.

Note: product detail routing stays collection-driven via the products collection route (`/shop/product/{slug}`), not by writing to `routes/web.php`.

## Shop Tags

- `{{ daugt_commerce:shop_products }}`:
filtered product loop (supports `search`, `category`, `sort`, `limit` via params or query string)
- `{{ daugt_commerce:shop_categories }}`:
category chips with counts and active state

Add usage docs once the first features land.
Backward compatibility:
- `statamic:daugt-commerce:make-store` remains available as an alias.
- `statamic:daugt-commerce:make-storefront` remains available as an alias.
- `store_products` and `store_categories` tags remain available as aliases.
- `storefront_products` and `storefront_categories` tags remain available as aliases.
20 changes: 20 additions & 0 deletions lang/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,26 @@
"daugt-commerce::cart.remove": "Entfernen",
"daugt-commerce::cart.quantity": "Anzahl: :count",
"daugt-commerce::cart.go-to-checkout": "Zahlungspflichtig bestellen",
"daugt-commerce::cart.close_panel": "Panel schließen",
"daugt-commerce::shop.title": "Shop",
"daugt-commerce::shop.subtitle": "Stöbere Produkte und lege sie in den Warenkorb.",
"daugt-commerce::shop.search": "Suchen",
"daugt-commerce::shop.search_placeholder": "Produkte suchen",
"daugt-commerce::shop.reset": "Zurücksetzen",
"daugt-commerce::shop.all_categories": "Alle Kategorien",
"daugt-commerce::shop.all": "Alle",
"daugt-commerce::shop.no_products": "Keine Produkte für die aktuellen Filter gefunden.",
"daugt-commerce::shop.view_product": "Produkt ansehen",
"daugt-commerce::shop.back_to_shop": "Zurück zum Shop",
"daugt-commerce::shop.buy_external": "Extern kaufen",
"daugt-commerce::shop.add_to_cart": "In den Warenkorb",
"daugt-commerce::shop.shipping_product": "Versandprodukt",
"daugt-commerce::shop.recurring_billing": "Wiederkehrende Abrechnung",
"daugt-commerce::shop.description": "Beschreibung",
"daugt-commerce::checkout.title": "Checkout",
"daugt-commerce::checkout.subtitle": "Schließe deine Bestellung sicher mit Stripe ab.",
"daugt-commerce::checkout.continue_shopping": "Weiter einkaufen",
"daugt-commerce::checkout.empty": "Dein Warenkorb ist leer.",
"daugt-commerce::address.title": "Adresse",
"daugt-commerce::address.fields.name": "Name",
"daugt-commerce::address.fields.phone": "Telefon",
Expand Down
20 changes: 20 additions & 0 deletions lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,26 @@
"daugt-commerce::cart.remove": "Remove",
"daugt-commerce::cart.quantity": "Qty :count",
"daugt-commerce::cart.go-to-checkout": "Go to checkout",
"daugt-commerce::cart.close_panel": "Close panel",
"daugt-commerce::shop.title": "Shop",
"daugt-commerce::shop.subtitle": "Browse products and add what you need to the cart.",
"daugt-commerce::shop.search": "Search",
"daugt-commerce::shop.search_placeholder": "Search products",
"daugt-commerce::shop.reset": "Reset",
"daugt-commerce::shop.all_categories": "All categories",
"daugt-commerce::shop.all": "All",
"daugt-commerce::shop.no_products": "No products found for your current filters.",
"daugt-commerce::shop.view_product": "View product",
"daugt-commerce::shop.back_to_shop": "Back to shop",
"daugt-commerce::shop.buy_external": "Buy external",
"daugt-commerce::shop.add_to_cart": "Add to cart",
"daugt-commerce::shop.shipping_product": "Shipping product",
"daugt-commerce::shop.recurring_billing": "Recurring billing",
"daugt-commerce::shop.description": "Description",
"daugt-commerce::checkout.title": "Checkout",
"daugt-commerce::checkout.subtitle": "Complete your order securely with Stripe.",
"daugt-commerce::checkout.continue_shopping": "Continue shopping",
"daugt-commerce::checkout.empty": "Your cart is empty.",
"daugt-commerce::address.title": "Address",
"daugt-commerce::address.fields.name": "Name",
"daugt-commerce::address.fields.phone": "Phone",
Expand Down
5 changes: 0 additions & 5 deletions resources/css/addon.css
Original file line number Diff line number Diff line change
@@ -1,6 +1 @@
/*
TODO: Revert back to statamic tailwind config for release (less CSS shipped)
@import "@statamic/cms/tailwind.css";
*/

@import "tailwindcss";
22 changes: 22 additions & 0 deletions resources/views/checkout.antlers.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<div class="container mx-auto mt-28 px-4 pb-12">
<div class="mb-6 flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
<div>
<h1 class="text-3xl font-bold text-neutral-800">{{ trans:daugt-commerce::checkout.title }}</h1>
<p class="mt-1 text-sm text-neutral-600">{{ trans:daugt-commerce::checkout.subtitle }}</p>
</div>

<a href="/shop" class="inline-flex items-center text-sm font-medium text-neutral-600 hover:text-neutral-900">
← {{ trans:daugt-commerce::checkout.continue_shopping }}
</a>
</div>

{{ if daugt_commerce:cart_count == "0" }}
<div class="rounded-xl border border-dashed border-neutral-300 bg-white p-10 text-center text-neutral-600">
{{ trans:daugt-commerce::checkout.empty }}
</div>
{{ else }}
<div class="overflow-hidden rounded-xl border border-neutral-200 bg-white">
{{ daugt_commerce:checkout return_url="{ current_url }" }}
</div>
{{ /if }}
</div>
56 changes: 56 additions & 0 deletions resources/views/products/product.antlers.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<div class="container mx-auto mt-28 px-4 pb-12">
<a href="/shop" class="inline-flex items-center text-sm font-medium text-neutral-600 hover:text-neutral-900">
← {{ trans:daugt-commerce::shop.back_to_shop }}
</a>

<div class="mt-8 grid grid-cols-1 gap-10 lg:grid-cols-5 lg:gap-12">
<div class="lg:col-span-3">
<div class="overflow-hidden rounded-lg border border-neutral-200 bg-neutral-50">
{{ if media[0] }}
<img src="{{ glide :src="media[0]" width="1200" height="820" fit="crop" }}" alt="{{ title }}" class="w-full object-cover" />
{{ else }}
<div class="aspect-[10/7] w-full bg-neutral-200"></div>
{{ /if }}
</div>
</div>

<div class="lg:col-span-2">
<h1 class="text-2xl font-bold text-neutral-800 sm:text-3xl">{{ title }}</h1>

<div class="mt-5 border-y border-neutral-300 py-4">
<div class="text-2xl font-bold text-neutral-800">{{ daugt_commerce:money :value="price" }}</div>
</div>

<div class="mt-4">
{{ if external_product && external_product_url }}
<a href="{{ external_product_url }}" target="_blank" rel="noopener" class="inline-flex items-center justify-center rounded-md bg-primary-600 px-4 py-2 text-sm font-medium text-white transition hover:bg-primary-700">
{{ trans:daugt-commerce::shop.buy_external }}
</a>
{{ else }}
{{ daugt_commerce:add_to_cart redirect="{ current_url }" }}
<button type="submit" data-cart-open class="inline-flex items-center justify-center rounded-md bg-primary-600 px-4 py-2 text-sm font-medium text-white transition hover:bg-primary-700">
{{ trans:daugt-commerce::shop.add_to_cart }}
</button>
{{ /daugt_commerce:add_to_cart }}
{{ /if }}
</div>

<ul class="mt-6 space-y-2 text-sm text-neutral-600">
{{ if shipping }}
<li>{{ trans:daugt-commerce::shop.shipping_product }}</li>
{{ /if }}

{{ if billing_type == "recurring" }}
<li>{{ trans:daugt-commerce::shop.recurring_billing }}</li>
{{ /if }}
</ul>
</div>

{{ if description }}
<div class="lg:col-span-3">
<h2 class="text-lg font-semibold text-neutral-800">{{ trans:daugt-commerce::shop.description }}</h2>
<div class="prose prose-neutral mt-3 max-w-none">{{ description }}</div>
</div>
{{ /if }}
</div>
</div>
104 changes: 104 additions & 0 deletions resources/views/shop/_cart.antlers.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<dialog id="daugt-commerce-cart" class="m-0 ml-auto h-dvh max-h-dvh w-full max-w-sm border-0 rounded-l-xl rounded-r-none bg-white p-4 shadow-xl backdrop:bg-black/30 backdrop:backdrop-blur-sm open:flex flex-col transition-opacity duration-200" closedby="any">
<div class="flex items-start justify-between">
<h2 id="drawer-title" class="text-lg font-medium text-gray-900">{{ trans:daugt-commerce::cart.title }}</h2>
<div class="ml-3 flex h-7 items-center">
<form method="dialog">
<button type="submit" class="relative -m-2 p-2 text-gray-400 hover:text-gray-500">
<span class="absolute -inset-0.5"></span>
<span class="sr-only">{{ trans:daugt-commerce::cart.close_panel }}</span>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" data-slot="icon" aria-hidden="true" class="size-6">
<path d="M6 18 18 6M6 6l12 12" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</button>
</form>
</div>
</div>
<ul role="list" class="mt-4 divide-y divide-gray-200 grow overflow-y-auto pr-1">
{{ daugt_commerce:cart_items }}
<li class="flex py-6">
<div class="size-24 shrink-0 overflow-hidden rounded-md border border-gray-200">
{{ if entry.media[0] }}
<img src="{{ glide :src="entry.media[0]" square="256" }}" alt="{{ entry.title }}" class="size-full object-cover" />
{{ else }}
<div class="size-full bg-gray-100"></div>
{{ /if }}
</div>

<div class="ml-4 flex flex-1 flex-col">
<div>
<div class="flex justify-between text-base font-medium text-gray-900">
<h3><a href="{{ entry.url }}">{{ entry.title }}</a></h3>
<p class="ml-4">{{ daugt_commerce:money :value="entry.price" }}</p>
</div>
<p class="mt-1 text-sm text-gray-500">
{{ if entry.shipping }}
{{ trans:daugt-commerce::products.fields.shipping }}
{{ /if }}
{{ if entry.billing_type == "recurring" }}
{{ trans:daugt-commerce::billing-types.recurring }}
{{ /if }}
</p>
</div>
<div class="flex flex-1 items-end justify-between text-sm">
<p class="text-gray-500">{{ trans:daugt-commerce::cart.quantity :count="quantity" }}</p>

<div class="flex">
{{ daugt_commerce:remove_from_cart :product_id="product_id" }}
<button class="font-medium text-primary-600 hover:text-primary-500">
{{ trans:daugt-commerce::cart.remove }}
</button>
{{ /daugt_commerce:remove_from_cart }}
</div>
</div>
</div>
</li>
{{ /daugt_commerce:cart_items }}
</ul>

<a class="button-primary mt-4" href="{{ link to="checkout" }}">
{{ trans:daugt-commerce::cart.go-to-checkout }}
</a>
</dialog>

<script>
(() => {
const CART_DIALOG_ID = 'daugt-commerce-cart';
const OPEN_AFTER_ADD_KEY = 'daugt-commerce-open-cart-after-add';

const getDialog = () => {
const dialog = document.getElementById(CART_DIALOG_ID);
return dialog instanceof HTMLDialogElement ? dialog : null;
};

const openDialog = () => {
const dialog = getDialog();
if (!dialog || dialog.open) return;
dialog.showModal();
};

window.daugtCommerceOpenCart = openDialog;

document.addEventListener('click', (event) => {
const trigger = event.target instanceof Element
? event.target.closest('[data-cart-open]')
: null;

if (!trigger) return;

event.preventDefault();
openDialog();
});

document.addEventListener('submit', (event) => {
const form = event.target;
if (!(form instanceof HTMLFormElement)) return;
if (!form.action.includes('/cart/add')) return;
sessionStorage.setItem(OPEN_AFTER_ADD_KEY, '1');
});

if (sessionStorage.getItem(OPEN_AFTER_ADD_KEY) === '1') {
sessionStorage.removeItem(OPEN_AFTER_ADD_KEY);
openDialog();
}
})();
</script>
69 changes: 69 additions & 0 deletions resources/views/shop/index.antlers.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<div class="container mx-auto mt-28 px-4 pb-12">
<div class="mb-6">
<h1 class="text-3xl font-bold text-neutral-800">{{ trans:daugt-commerce::shop.title }}</h1>
<p class="mt-1 text-sm text-neutral-600">{{ trans:daugt-commerce::shop.subtitle }}</p>
</div>

<form method="GET" class="flex flex-col gap-3 md:flex-row md:items-center">
<input
type="search"
name="search"
value="{{ get:search }}"
placeholder="{{ trans:daugt-commerce::shop.search_placeholder }}"
class="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm text-neutral-700 transition focus:border-primary-500 focus:outline-none"
/>

{{ if get:category }}
<input type="hidden" name="category" value="{{ get:category }}" />
{{ /if }}

<button type="submit" class="rounded-md bg-primary-600 px-4 py-2 text-sm font-medium text-white transition hover:bg-primary-700">
{{ trans:daugt-commerce::shop.search }}
</button>

</form>

<div class="mt-4 flex flex-wrap gap-2">
<a
href="{{ current_url }}"
class="rounded-full border px-3 py-1 text-sm transition {{ if !get:category }}border-primary-600 bg-primary-600 text-white{{ else }}border-neutral-300 bg-white text-neutral-700 hover:border-primary-500 hover:text-primary-700{{ /if }}"
>
{{ trans:daugt-commerce::shop.all }}
</a>

{{ daugt_commerce:shop_categories }}
<a
href="{{ url }}"
class="rounded-full border px-3 py-1 text-sm transition {{ if active }}border-primary-600 bg-primary-600 text-white{{ else }}border-neutral-300 bg-white text-neutral-700 hover:border-primary-500 hover:text-primary-700{{ /if }}"
>
{{ title }}
</a>
{{ /daugt_commerce:shop_categories }}
</div>

<div class="mt-8 grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{{ daugt_commerce:shop_products search="{get:search}" category="{get:category}" }}
{{ if no_results }}
<div class="col-span-full rounded-xl border border-dashed border-neutral-300 bg-white p-10 text-center text-neutral-600">
{{ trans:daugt-commerce::shop.no_products }}
</div>
{{ else }}
<a href="{{ url }}" class="group flex h-full w-full flex-col overflow-hidden rounded-md border-2 border-neutral-200 bg-neutral-50 transition hover:border-primary-500">
{{ if image }}
<img src="{{ glide :src="image" width="1000" height="700" fit="crop" }}" alt="{{ title }}" class="aspect-[10/7] w-full object-cover group-hover:opacity-75" />
{{ else }}
<div class="aspect-[10/7] w-full bg-neutral-200"></div>
{{ /if }}

<div class="flex flex-1 flex-col p-3">
<h2 class="truncate text-sm font-semibold text-neutral-800 group-hover:text-primary-700 md:text-base">{{ title }}</h2>
<p class="mt-1 text-base font-medium text-neutral-700 md:text-lg">{{ daugt_commerce:money :value="price" }}</p>
{{ if description_preview }}
<p class="mt-2 line-clamp-2 text-sm text-neutral-600">{{ description_preview }}</p>
{{ /if }}
</div>
</a>
{{ /if }}
{{ /daugt_commerce:shop_products }}
</div>
</div>
Loading