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
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