Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,6 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts

# local agent/tool state
.serena/
47 changes: 47 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ This document defines the mandatory rules and constraints for the AI agent worki
- DO NOT modify, delete, or rewrite existing Prisma migration files on `main`; create a new migration for schema changes instead.
- AI MUST NOT take autonomous business decisions (suggestions only).
- **No commits or PRs without explicit permission**: never run git commit, git push, or create PRs unless the user explicitly requests it. Changes must remain in the staging area/working tree until the user decides otherwise.
- **No issue required for PRs**: this repository does not require a GitHub issue to open a pull request. If a PR template asks for issue linkage, mark it as not applicable instead of blocking PR creation.
- **Never auto-complete PRs**: always stop and ASK before merging. Never merge PRs automatically, even if all checks pass. The user must explicitly approve every merge.

---
Expand Down Expand Up @@ -68,3 +69,49 @@ This document defines the mandatory rules and constraints for the AI agent worki
- Event Bus: `docs/event-bus.md`
- Entity Model: `docs/entities.md`
- Modules: See `docs/module-*.md`

<!-- headroom:rtk-instructions -->

# RTK (Rust Token Killer) - Token-Optimized Commands

When running shell commands, **always prefix with `rtk`**. This reduces context
usage by 60-90% with zero behavior change. If rtk has no filter for a command,
it passes through unchanged — so it is always safe to use.

## Key Commands

```bash
# Git (59-80% savings)
rtk git status rtk git diff rtk git log

# Files & Search (60-75% savings)
rtk ls <path> rtk read <file> rtk grep <pattern>
rtk find <pattern> rtk diff <file>

# Test (90-99% savings) — shows failures only
rtk pytest tests/ rtk cargo test rtk test <cmd>

# Build & Lint (80-90% savings) — shows errors only
rtk tsc rtk lint rtk cargo build
rtk prettier --check rtk mypy rtk ruff check

# Analysis (70-90% savings)
rtk err <cmd> rtk log <file> rtk json <file>
rtk summary <cmd> rtk deps rtk env

# GitHub (26-87% savings)
rtk gh pr view <n> rtk gh run list rtk gh issue list

# Infrastructure (85% savings)
rtk docker ps rtk kubectl get rtk docker logs <c>

# Package managers (70-90% savings)
rtk pip list rtk pnpm install rtk npm run <script>
```

## Rules

- In command chains, prefix each segment: `rtk git add . && rtk git commit -m "msg"`
- For debugging, use raw command without rtk prefix
- `rtk proxy <cmd>` runs command without filtering but tracks usage
<!-- /headroom:rtk-instructions -->
64 changes: 63 additions & 1 deletion app/[locale]/cart/cart-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export interface CartItemDTO {
color: string | null;
size: string | null;
imageUrl: string | null;
imageUploadId?: string | null;
};
}

Expand Down Expand Up @@ -56,9 +57,40 @@ interface CartViewProps {
customizationSize: string;
customizationColor: string;
customizationText: string;
customizationEditFromCart?: string;
};
}

function buildCustomizationHref(
locale: string,
productId: string,
customization: CartItemDTO['customization'],
): string | null {
const hasCustomization =
customization.text ||
customization.color ||
customization.size ||
customization.imageUrl ||
customization.imageUploadId;

if (!hasCustomization) {
return null;
}

const params = new URLSearchParams();
if (customization.text) params.set('customizationText', customization.text);
if (customization.color)
params.set('customizationColor', customization.color);
if (customization.size) params.set('customizationSize', customization.size);
if (customization.imageUrl)
params.set('customizationImageUrl', customization.imageUrl);
if (customization.imageUploadId)
params.set('customizationImageUploadId', customization.imageUploadId);

const query = params.toString();
return `/${locale}/products/${productId}${query ? `?${query}` : ''}`;
}

/**
* Client component for the cart page.
*
Expand Down Expand Up @@ -106,6 +138,7 @@ export function CartView({
color: gi.customizationColor ?? null,
size: gi.customizationSize ?? null,
imageUrl: gi.customizationImageUrl ?? null,
imageUploadId: gi.customizationImageUploadId ?? null,
},
}));

Expand Down Expand Up @@ -233,7 +266,8 @@ export function CartView({
</span>
{(item.customization.text ||
item.customization.color ||
item.customization.size) && (
item.customization.size ||
item.customization.imageUrl) && (
<span className={styles.customization}>
{[
item.customization.size &&
Expand All @@ -247,6 +281,34 @@ export function CartView({
.join(' · ')}
</span>
)}
{labels.customizationEditFromCart &&
buildCustomizationHref(
locale,
item.productId,
item.customization,
) && (
<a
href={
buildCustomizationHref(
locale,
item.productId,
item.customization,
) ?? '#'
}
className={styles.editLink}
>
{labels.customizationEditFromCart}
</a>
)}
{item.customization.imageUrl && (
<Image
src={item.customization.imageUrl}
alt="Customization preview"
width={48}
height={48}
className={styles.customizationThumbnail}
/>
)}
</div>
</div>

Expand Down
53 changes: 41 additions & 12 deletions app/[locale]/cart/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { GetCart } from '@/modules/cart/application/get-cart';
import { getDictionary } from '@/shared/i18n/get-dictionary';
import { CartView, type CartItemDTO } from './cart-view';
import type { ProductEntity } from '@/modules/products/domain/product-repository';
import type { CustomizationSnapshot } from '@/modules/cart/domain/customization-lookup-port';

/**
* Cart page — RSC shell.
Expand All @@ -31,23 +32,41 @@ export default async function CartPage({

if (isAuthenticated) {
const cartRepository = container.getCartRepository();
const customizationLookup = container.getCustomizationLookup();
const getCart = new GetCart(cartRepository);
const cart = await getCart.execute(session.user.id);

if (cart.items.length > 0) {
// Enrich items with product display data.
const productRepository = container.getProductRepository();
const productIds = [...new Set(cart.items.map((i) => i.productId.value))];
const products = await Promise.all(
productIds.map((id) => productRepository.findById(id, locale)),
);
const productMap = new Map<string, ProductEntity>();
products.forEach((p) => {
if (p) productMap.set(p.id, p);
});
for (const id of productIds) {
const product = await productRepository.findById(id, locale);
if (product) productMap.set(product.id, product);
}

const customizationIds = [
...new Set(cart.items.flatMap((item) => item.customizationIdList)),
];
const customizations =
customizationIds.length > 0
? await customizationLookup.findByIds(customizationIds)
: [];
const customizationMap = new Map<string, CustomizationSnapshot>(
customizations.map((item) => [item.id, item]),
);

items = cart.items.map((item) => {
const product = productMap.get(item.productId.value);
const firstCustomization =
item.customizationIdList
.map((id) => customizationMap.get(id))
.find(
(customization): customization is CustomizationSnapshot =>
customization != null,
) ?? null;

return {
id: item.id,
productId: item.productId.value,
Expand All @@ -60,12 +79,21 @@ export default async function CartPage({
lineTotal: +(item.unitPriceSnapshot.amount * item.quantity).toFixed(
2,
),
customization: {
text: null,
color: null,
size: null,
imageUrl: null,
},
customization: firstCustomization
? {
text: firstCustomization.text,
color: firstCustomization.color,
size: firstCustomization.size,
imageUrl: firstCustomization.imageUrl,
imageUploadId: null,
}
: {
text: null,
color: null,
size: null,
imageUrl: null,
imageUploadId: null,
},
};
});
}
Expand All @@ -90,6 +118,7 @@ export default async function CartPage({
customizationSize: dict.common.customizationSize,
customizationColor: dict.common.customizationColor,
customizationText: dict.common.customizationText,
customizationEditFromCart: dict.common.customizationEditFromCart,
}}
/>
);
Expand Down
20 changes: 14 additions & 6 deletions app/[locale]/checkout/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { ProductEntity } from '@/modules/products/domain/product-repository
import { Money } from '@/shared/kernel/domain/value-objects/money';
import { Currency } from '@/shared/kernel/domain/value-objects/currency';
import type { CustomizationSnapshot } from '@/modules/cart/domain/customization-lookup-port';
import Image from 'next/image';
import styles from './page.module.css';

interface CheckoutItem {
Expand Down Expand Up @@ -75,13 +76,11 @@ export default async function CheckoutPage({
// Enrich items with product display data.
const productRepository = container.getProductRepository();
const productIds = [...new Set(cart.items.map((i) => i.productId.value))];
const products = await Promise.all(
productIds.map((id) => productRepository.findById(id, locale)),
);
const productMap = new Map<string, ProductEntity>();
products.forEach((p) => {
if (p) productMap.set(p.id, p);
});
for (const id of productIds) {
const product = await productRepository.findById(id, locale);
if (product) productMap.set(product.id, product);
}

const currency = cart.items[0].unitPriceSnapshot.currency;
const hasMixedCurrencies = cart.items.some(
Expand Down Expand Up @@ -197,6 +196,15 @@ export default async function CheckoutPage({
.join(' · ')}
</span>
)}
{item.customizations[0]?.imageUrl && (
<Image
src={item.customizations[0].imageUrl}
alt="Customization preview"
width={48}
height={48}
className={styles.itemCustomizationThumbnail}
/>
)}
{item.customizationIdList.length >
item.customizations.length && (
<span className={styles.itemCustomizationRemoved}>
Expand Down
Loading
Loading