diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 00000000..0867ee01 --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,6 @@ +# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json +reviews: + auto_review: + enabled: true + base_branches: + - develop diff --git a/.github/workflows/shop-janitor-restock-stale.yml b/.github/workflows/shop-janitor-restock-stale.yml index a5b70d6c..c304db38 100644 --- a/.github/workflows/shop-janitor-restock-stale.yml +++ b/.github/workflows/shop-janitor-restock-stale.yml @@ -2,7 +2,7 @@ name: Shop janitor - restock stale orders on: schedule: - - cron: "*/5 * * * *" # every 5 minutes (UTC) + - cron: "*/5 * * * *" workflow_dispatch: {} concurrency: @@ -30,7 +30,6 @@ jobs: with: node-version: "20" - # Step-level guard: тут secrets дозволені - name: Guard config (skip if secrets missing) id: guard run: | diff --git a/frontend/app/[locale]/shop/admin/orders/page.tsx b/frontend/app/[locale]/shop/admin/orders/page.tsx index 7979aa7f..460f55a6 100644 --- a/frontend/app/[locale]/shop/admin/orders/page.tsx +++ b/frontend/app/[locale]/shop/admin/orders/page.tsx @@ -20,7 +20,6 @@ export const metadata: Metadata = { description: 'View and manage orders in the DevLovers shop catalog.', }; - export const dynamic = 'force-dynamic'; const PAGE_SIZE = 25; @@ -61,7 +60,6 @@ export default async function AdminOrdersPage({ const page = parsePage(sp.page); const offset = (page - 1) * PAGE_SIZE; - // overfetch for hasNext without COUNT const { items: all } = await getAdminOrdersPage({ limit: PAGE_SIZE + 1, offset, @@ -147,12 +145,16 @@ export default async function AdminOrdersPage({
-
{t('table.items')}
+
+ {t('table.items')} +
{vm.itemCount}
-
{t('table.provider')}
+
+ {t('table.provider')} +
-
{t('table.orderId')}
+
+ {t('table.orderId')} +
> >({}); - // Hydrate state from initialValues once per product in EDIT mode. - // In edit: slug must come from DB and stay stable (no title->slug regeneration). useEffect(() => { if (mode !== 'edit') { hydratedKeyRef.current = null; @@ -221,8 +219,6 @@ export function ProductForm({ if (hydratedKeyRef.current === key) return; - // Reset transient UI state when switching between products in EDIT mode. - // Do NOT do this in submit: it breaks retries (e.g., clears selected image). setError(null); setSlugError(null); setImageError(null); @@ -251,8 +247,7 @@ export function ProductForm({ }, [mode, initialValues, productId]); const slugValue = useMemo(() => { - if (mode === 'edit') return slug; // slug в edit має бути стабільним (з БД) - // In create mode, always derive from current title to avoid stale slug on fast submit. + if (mode === 'edit') return slug; return localSlugify(title); }, [mode, slug, title]); @@ -967,8 +962,8 @@ export function ProductForm({ ? 'Creating...' : 'Updating...' : mode === 'create' - ? 'Create product' - : 'Save changes'} + ? 'Create product' + : 'Save changes'} diff --git a/frontend/app/[locale]/shop/cart/CartPageClient.tsx b/frontend/app/[locale]/shop/cart/CartPageClient.tsx index acf8dc50..9fb12085 100644 --- a/frontend/app/[locale]/shop/cart/CartPageClient.tsx +++ b/frontend/app/[locale]/shop/cart/CartPageClient.tsx @@ -422,7 +422,6 @@ export default function CartPage() { {t('checkout.message')}

- {/* Fallback CTA if navigation fails after order was created */} {createdOrderId && !checkoutError ? (
DO NOT prefix locale manually. - * - Stripe return_url is an external redirect -> MUST include locale exactly once. - */ const IN_APP_SHOP_BASE = '/shop'; function normalizeLocale(raw: string): string { @@ -73,13 +72,21 @@ function buildInAppPath(path: string): string { return `${IN_APP_SHOP_BASE}${p}`; } -function buildStripeReturnUrl(params: { locale: string; inAppPath: string }): string { +function buildStripeReturnUrl(params: { + locale: string; + inAppPath: string; +}): string { const loc = normalizeLocale(params.locale); - const p = params.inAppPath.startsWith('/') ? params.inAppPath : `/${params.inAppPath}`; + const p = params.inAppPath.startsWith('/') + ? params.inAppPath + : `/${params.inAppPath}`; return new URL(`/${loc}${p}`, window.location.origin).toString(); } -function nextRouteForPaymentResult(params: { orderId: string; status?: string | null }) { +function nextRouteForPaymentResult(params: { + orderId: string; + status?: string | null; +}) { const { orderId, status } = params; const id = encodeURIComponent(orderId); @@ -88,7 +95,11 @@ function nextRouteForPaymentResult(params: { orderId: string; status?: string | if (!status) return success; - if (status === 'succeeded' || status === 'processing' || status === 'requires_capture') { + if ( + status === 'succeeded' || + status === 'processing' || + status === 'requires_capture' + ) { return success; } @@ -99,7 +110,6 @@ function nextRouteForPaymentResult(params: { orderId: string; status?: string | return success; } -/** Unified CTA (hero) */ const SHOP_HERO_CTA = cn( SHOP_CTA_BASE, SHOP_CTA_INTERACTIVE, @@ -110,7 +120,6 @@ const SHOP_HERO_CTA = cn( 'shadow-[var(--shop-hero-btn-shadow)] hover:shadow-[var(--shop-hero-btn-shadow-hover)]' ); -/** Unified outline */ const SHOP_OUTLINE = cn( SHOP_OUTLINE_BTN_BASE, SHOP_OUTLINE_BTN_INTERACTIVE, @@ -122,19 +131,22 @@ const SHOP_OUTLINE = cn( function HeroCtaInner({ children }: { children: React.ReactNode }) { return ( <> - {/* base gradient */}
-

{uiCurrency}

+

+ {uiCurrency} +

diff --git a/frontend/app/[locale]/shop/checkout/payment/[orderId]/page.tsx b/frontend/app/[locale]/shop/checkout/payment/[orderId]/page.tsx index 182dc50d..76f1139c 100644 --- a/frontend/app/[locale]/shop/checkout/payment/[orderId]/page.tsx +++ b/frontend/app/[locale]/shop/checkout/payment/[orderId]/page.tsx @@ -97,7 +97,6 @@ function HeroCtaLink({ }) { return ( - {/* base gradient */}