Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
2 changes: 1 addition & 1 deletion frontend/app/[locale]/shop/admin/orders/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
type CurrencyCode,
} from '@/lib/shop/currency';
import { fromDbMoney } from '@/lib/shop/money';
import { ShopAdminTopbar } from '@/components/shop/admin/shop-admin-topbar';
import { ShopAdminTopbar } from '@/components/shop/admin/ShopAdminTopbar';
import { guardShopAdminPage } from '@/lib/auth/guard-shop-admin-page';
import { Metadata } from 'next';

Expand Down
4 changes: 2 additions & 2 deletions frontend/app/[locale]/shop/admin/orders/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import {
type CurrencyCode,
} from '@/lib/shop/currency';
import { fromDbMoney } from '@/lib/shop/money';
import { ShopAdminTopbar } from '@/components/shop/admin/shop-admin-topbar';
import { AdminPagination } from '@/components/shop/admin/admin-pagination';
import { ShopAdminTopbar } from '@/components/shop/admin/ShopAdminTopbar';
import { AdminPagination } from '@/components/shop/admin/AdminPagination';
import { guardShopAdminPage } from '@/lib/auth/guard-shop-admin-page';
import { CSRF_FORM_FIELD, issueCsrfToken } from '@/lib/security/csrf';
import { parsePage } from '@/lib/pagination';
Expand Down
2 changes: 1 addition & 1 deletion frontend/app/[locale]/shop/admin/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Link } from '@/i18n/routing';
import { getTranslations } from 'next-intl/server';

import { ShopAdminTopbar } from '@/components/shop/admin/shop-admin-topbar';
import { ShopAdminTopbar } from '@/components/shop/admin/ShopAdminTopbar';
import { guardShopAdminPage } from '@/lib/auth/guard-shop-admin-page';
import { Metadata } from 'next';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { eq } from 'drizzle-orm';
import { z } from 'zod';

import { guardShopAdminPage } from '@/lib/auth/guard-shop-admin-page';
import { ShopAdminTopbar } from '@/components/shop/admin/shop-admin-topbar';
import { ShopAdminTopbar } from '@/components/shop/admin/ShopAdminTopbar';

import { ProductForm } from '../../_components/product-form';
import { ProductForm } from '../../_components/ProductForm';
import { db } from '@/db';
import { products, productPrices } from '@/db/schema';
import type { CurrencyCode } from '@/lib/shop/currency';
Expand Down
4 changes: 2 additions & 2 deletions frontend/app/[locale]/shop/admin/products/new/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { ShopAdminTopbar } from '@/components/shop/admin/shop-admin-topbar';
import { ShopAdminTopbar } from '@/components/shop/admin/ShopAdminTopbar';
import { guardShopAdminPage } from '@/lib/auth/guard-shop-admin-page';
import { issueCsrfToken } from '@/lib/security/csrf';

import { ProductForm } from '../_components/product-form';
import { ProductForm } from '../_components/ProductForm';
import { Metadata } from 'next';

export const metadata: Metadata = {
Expand Down
8 changes: 4 additions & 4 deletions frontend/app/[locale]/shop/admin/products/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Link } from '@/i18n/routing';
import { and, desc, eq, sql } from 'drizzle-orm';
import { issueCsrfToken } from '@/lib/security/csrf';
import { ShopAdminTopbar } from '@/components/shop/admin/shop-admin-topbar';
import { ShopAdminTopbar } from '@/components/shop/admin/ShopAdminTopbar';
import { guardShopAdminPage } from '@/lib/auth/guard-shop-admin-page';
import { AdminProductDeleteButton } from '@/components/shop/admin/admin-product-delete-button';
import { AdminProductStatusToggle } from '@/components/shop/admin/admin-product-status-toggle';
import { AdminPagination } from '@/components/shop/admin/admin-pagination';
import { AdminProductDeleteButton } from '@/components/shop/admin/AdminProductDeleteButton';
import { AdminProductStatusToggle } from '@/components/shop/admin/AdminProductStatusToggle';
import { AdminPagination } from '@/components/shop/admin/AdminPagination';
import { db } from '@/db';
import {
inventoryMoves,
Expand Down
2 changes: 1 addition & 1 deletion frontend/app/[locale]/shop/cart/CartPageClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { useTranslations } from 'next-intl';
import { cn } from '@/lib/utils';
import { Minus, Plus, Trash2, ShoppingBag } from 'lucide-react';

import { useCart } from '@/components/shop/cart-provider';
import { useCart } from '@/components/shop/CartProvider';
import { generateIdempotencyKey } from '@/lib/shop/idempotency';
import { formatMoney } from '@/lib/shop/currency';
import {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Link } from '@/i18n/routing';
import { ClearCartOnMount } from '@/components/shop/clear-cart-on-mount';
import { ClearCartOnMount } from '@/components/shop/ClearCartOnMount';
import StripePaymentClient from '../StripePaymentClient';

import { formatMoney } from '@/lib/shop/currency';
Expand Down
3 changes: 1 addition & 2 deletions frontend/app/[locale]/shop/checkout/success/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Link } from '@/i18n/routing';
import { getTranslations } from 'next-intl/server';

import OrderStatusAutoRefresh from './OrderStatusAutoRefresh';
import { ClearCartOnMount } from '@/components/shop/clear-cart-on-mount';
import { ClearCartOnMount } from '@/components/shop/ClearCartOnMount';
import { formatMoney } from '@/lib/shop/currency';
import { getOrderSummary } from '@/lib/services/orders';
import { OrderNotFoundError } from '@/lib/services/errors';
Expand Down Expand Up @@ -57,7 +57,6 @@ function shouldClearCart(params: SearchParams): boolean {
return raw === 'true' || raw === '1';
}

/** Small hero CTA (Link) */
const SHOP_HERO_CTA_SM = cn(
SHOP_CTA_BASE,
SHOP_CTA_INTERACTIVE,
Expand Down
6 changes: 3 additions & 3 deletions frontend/app/[locale]/shop/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Link } from '@/i18n/routing';
import { ProductCard } from '@/components/shop/product-card';
import { Hero } from '@/components/shop/shop-hero';
import { CategoryTile } from '@/components/shop/category-tile';
import { ProductCard } from '@/components/shop/ProductCard';
import { Hero } from '@/components/shop/ShopHero';
import { CategoryTile } from '@/components/shop/CategoryTile';
import { getHomepageContent } from '@/lib/shop/data';
import { getTranslations } from 'next-intl/server';
import {
Expand Down
2 changes: 1 addition & 1 deletion frontend/app/[locale]/shop/products/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { notFound } from 'next/navigation';
import { ArrowLeft } from 'lucide-react';
import { getTranslations } from 'next-intl/server';
import { cn } from '@/lib/utils';
import { AddToCartButton } from '@/components/shop/add-to-cart-button';
import { AddToCartButton } from '@/components/shop/AddToCartButton';
import { getProductPageData } from '@/lib/shop/data';
import { formatMoney, resolveCurrencyFromLocale } from '@/lib/shop/currency';
import { getPublicProductBySlug } from '@/db/queries/shop/products';
Expand Down
6 changes: 3 additions & 3 deletions frontend/app/[locale]/shop/products/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { Suspense } from 'react';
import { redirect } from 'next/navigation';
import { getTranslations } from 'next-intl/server';

import { ProductFilters } from '@/components/shop/product-filters';
import { CatalogProductsClient } from '@/components/shop/catalog-products-client';
import { ProductsToolbar } from '@/components/shop/products-toolbar';
import { ProductFilters } from '@/components/shop/ProductFilters';
import { CatalogProductsClient } from '@/components/shop/CatalogProductsClient';
import { ProductsToolbar } from '@/components/shop/ProductsToolbar';
import { getCatalogProducts } from '@/lib/shop/data';
import { catalogQuerySchema } from '@/lib/validation/shop';
import { CATALOG_PAGE_SIZE } from '@/lib/config/catalog';
Expand Down
9 changes: 7 additions & 2 deletions frontend/app/api/shop/webhooks/stripe/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1518,9 +1518,14 @@ export async function POST(request: NextRequest) {
isNull(stripeEvents.processedAt)
)
);
} catch {
// best-effort
} catch (err) {
logWarn('stripe_webhook_claim_release_failed', {
...eventMeta(),
code: 'CLAIM_RELEASE_FAILED',
message: err instanceof Error ? err.message : String(err),
});
}

return noStoreJson({ error: 'internal_error' }, { status: 500 });
}
}
2 changes: 1 addition & 1 deletion frontend/components/header/AppChrome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import React from 'react';
import { useSelectedLayoutSegments } from 'next/navigation';

import { UnifiedHeader } from '@/components/header/UnifiedHeader';
import { CartProvider } from '@/components/shop/cart-provider';
import { CartProvider } from '@/components/shop/CartProvider';

type AppChromeProps = {
userExists: boolean;
Expand Down
2 changes: 1 addition & 1 deletion frontend/components/header/DesktopActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { HeaderButton } from '@/components/shared/HeaderButton';
import { GitHubStarButton } from '@/components/shared/GitHubStarButton';
import LanguageSwitcher from '@/components/shared/LanguageSwitcher';
import { LogoutButton } from '@/components/auth/logoutButton';
import { CartButton } from '@/components/shop/header/cart-button';
import { CartButton } from '@/components/shop/header/CartButton';
import { BlogHeaderSearch } from '@/components/blog/BlogHeaderSearch';

type DesktopActionsProps = {
Expand Down
2 changes: 1 addition & 1 deletion frontend/components/header/DesktopNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { SITE_LINKS } from '@/lib/navigation';

import { NavLink } from '@/components/header/NavLink';
import { HeaderButton } from '@/components/shared/HeaderButton';
import { NavLinks } from '@/components/shop/header/nav-links';
import { NavLinks } from '@/components/shop/header/NavLinks';
import { BlogCategoryLinks } from '@/components/blog/BlogCategoryLinks';

type Category = {
Expand Down
2 changes: 1 addition & 1 deletion frontend/components/header/MobileActions.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';

import LanguageSwitcher from '@/components/shared/LanguageSwitcher';
import { CartButton } from '@/components/shop/header/cart-button';
import { CartButton } from '@/components/shop/header/CartButton';
import { BlogHeaderSearch } from '@/components/blog/BlogHeaderSearch';
import { AppMobileMenu } from '@/components/header/AppMobileMenu';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
SHOP_STEPPER_BUTTON_BASE,
} from '@/lib/shop/ui-classes';

import { useCart } from './cart-provider';
import { useCart } from './CartProvider';

interface AddToCartButtonProps {
product: ShopProduct;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import React from 'react';
import { useSearchParams, type ReadonlyURLSearchParams } from 'next/navigation';
import { useTranslations } from 'next-intl';

import { CatalogLoadMore } from '@/components/shop/catalog-load-more';
import { ProductCard } from '@/components/shop/product-card';
import { CatalogLoadMore } from '@/components/shop/CatalogLoadMore';
import { ProductCard } from '@/components/shop/ProductCard';
import { logError } from '@/lib/logging';

type Product = React.ComponentProps<typeof ProductCard>['product'] & {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';

import { useEffect, useRef } from 'react';
import { useCart } from '@/components/shop/cart-provider';
import { useCart } from '@/components/shop/CartProvider';

type ClearCartOnMountProps = {
enabled?: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export function ProductFilters() {
SHOP_FOCUS,
currentCategory === cat.slug
? 'text-accent'
: 'text-muted-foreground hover:text-accent'
: 'text-muted-foreground hover:text-accent active:text-accent'
)}
>
{tCategories(
Expand Down Expand Up @@ -109,7 +109,7 @@ export function ProductFilters() {
SHOP_FOCUS,
isSelected
? 'text-accent'
: 'text-muted-foreground hover:text-accent'
: 'text-muted-foreground hover:text-accent active:text-accent'
)}
>
{tTypes(type.slug)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import React from 'react';
import { Filter, X } from 'lucide-react';
import { useTranslations } from 'next-intl';

import { ProductSort } from '@/components/shop/product-sort';
import { ProductFilters } from '@/components/shop/product-filters';
import { ProductSort } from '@/components/shop/ProductSort';
import { ProductFilters } from '@/components/shop/ProductFilters';

export function ProductsToolbar() {
const [open, setOpen] = React.useState(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { ShoppingBag } from 'lucide-react';
import { useMounted } from '@/hooks/use-mounted';
import { HeaderButton } from '@/components/shared/HeaderButton';

import { useCart } from '../cart-provider';
import { useCart } from '../CartProvider';

export function CartButton() {
const { cart } = useCart();
Expand Down
26 changes: 6 additions & 20 deletions frontend/lib/admin/parseAdminProductForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ function parseMajorToMinor(
typeof value === 'string'
? value.trim()
: typeof value === 'number'
? String(value)
: '';
? String(value)
: '';

if (!raw) return null;

Expand All @@ -104,12 +104,6 @@ function parseLegacyPriceMinorField(
return toCents(v);
}

/**
* Legacy optional field semantics:
* - if field missing => undefined (PATCH omit)
* - if present but empty => null (explicit clear)
* - if present and value => cents int
*/
function parseLegacyOptionalOriginalMinorField(
formData: FormData,
name: string
Expand All @@ -135,8 +129,8 @@ function parseMinorInt(
typeof value === 'number'
? value
: typeof value === 'string'
? Number(value.trim())
: NaN;
? Number(value.trim())
: NaN;

if (!Number.isFinite(raw) || !Number.isInteger(raw) || raw < 0) {
throw zodPricesJsonError(`Invalid ${opts.field} for ${opts.currency}`);
Expand All @@ -149,7 +143,6 @@ function requirePositivePriceMinor(
priceMinor: number | null,
currency: string
) {
// DB check: priceMinor > 0
if (priceMinor == null || priceMinor <= 0) {
throw zodPricesJsonError(`Missing price for ${currency}`);
}
Expand Down Expand Up @@ -206,13 +199,11 @@ function parsePricesJsonField(formData: FormData, mode: ParseMode) {
throw zodPricesJsonError('Invalid currency in prices payload');
}

// Prefer canonical minor payload
let priceMinor = parseMinorInt(row?.priceMinor, {
field: 'priceMinor',
currency: currency as string,
});

// Legacy major fallback
if (priceMinor == null) {
priceMinor = parseMajorToMinor(row?.price, {
field: 'price',
Expand Down Expand Up @@ -240,10 +231,8 @@ function parsePricesJsonField(formData: FormData, mode: ParseMode) {
});
}

// Normalize: empty -> null
if (originalPriceMinor == null) originalPriceMinor = null;

// DB invariant: originalPriceMinor is null OR > priceMinor
if (originalPriceMinor !== null && priceMinor != null) {
if (originalPriceMinor <= priceMinor) {
throw zodPricesJsonError(
Expand Down Expand Up @@ -285,13 +274,11 @@ export function parseAdminProductForm(
> {
const mode: ParseMode = options.mode ?? 'create';

// 1) Prefer canonical "prices" JSON payload if present
const pricesJson = parsePricesJsonField(formData, mode);
if (pricesJson && 'ok' in pricesJson && pricesJson.ok === false) {
return { ok: false, error: pricesJson.error };
}

// 2) Legacy fallback (priceUsd/priceUah) -> MINOR units
const priceUsdMinor = parseLegacyPriceMinorField(formData, 'priceUsd');
const originalPriceUsdMinor = parseLegacyOptionalOriginalMinorField(
formData,
Expand Down Expand Up @@ -325,13 +312,12 @@ export function parseAdminProductForm(
: []),
];

// Resolve final prices with PATCH semantics
const prices =
pricesJson && 'value' in pricesJson
? pricesJson.value
: mode === 'update' && legacyRawPrices.length === 0
? undefined
: legacyRawPrices;
? undefined
: legacyRawPrices;

const payload = {
title: getStringField(formData, 'title'),
Expand Down
Loading