Skip to content

feat: bonus program registration#135

Merged
hmbanan666 merged 1 commit into
mainfrom
bonus-program
Sep 5, 2025
Merged

feat: bonus program registration#135
hmbanan666 merged 1 commit into
mainfrom
bonus-program

Conversation

@hmbanan666
Copy link
Copy Markdown
Collaborator

@hmbanan666 hmbanan666 commented Sep 5, 2025

Summary by CodeRabbit

  • New Features

    • New Client profile page with points card.
    • Bonus program activation flow with form (name, surname, email, birth date) and celebratory confetti.
    • Level progress indicator for next cashback tier.
    • Background confetti overlay.
    • Basic Navigation page placeholder.
  • Improvements

    • Navigation updated to use /client; added return icon for nested client routes.
    • More precise scroll-to-top behavior on home.
    • Consistent drawer visuals and minor copy tweaks.
  • Localization

    • Added Russian labels for surname, email, and birth date.
  • Chores

    • Updated TypeScript version in workspace catalog.

@hmbanan666 hmbanan666 self-assigned this Sep 5, 2025
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Sep 5, 2025

Walkthrough

Adds client profile routing and UI, introduces a bonus program registration flow with form, server API, DB fields/repo method, and store updates. Centralizes UDrawer UI via app config. Adds confetti and gyroscope shared composables, integrates confetti into layout and form success. Adjusts navigation and initialization guards. Updates TypeScript version.

Changes

Cohort / File(s) Summary of changes
Drawer UI centralization
apps/storefront-telegram/app/app.config.ts, apps/storefront-telegram/app/components/CitySelector.vue
Added ui.drawer.slots.content config; removed per-instance :ui height on UDrawer in CitySelector.
Navigation and routes
apps/storefront-telegram/app/components/Navigation.vue, apps/storefront-telegram/app/components/NavigationButton.vue, apps/storefront-telegram/app/pages/user.vue, apps/storefront-telegram/app/pages/client/index.vue, apps/storefront-telegram/app/pages/navigation.vue
Updated routes (/user/client, /secret1/navigation). Added nested-client return icon/logic and stricter scroll-to-top. Deleted user.vue; added client/index.vue and a simple navigation.vue.
Confetti system
apps/storefront-telegram/app/components/background/Confetti.vue, apps/storefront-telegram/app/composables/useConfetti.ts, apps/storefront-telegram/app/layouts/default.vue
Added shared confetti composable and full-screen confetti component; mounted in default layout.
Bonus program registration feature
apps/storefront-telegram/app/components/client/BonusProgramRegistration.vue, apps/storefront-telegram/app/components/form/CompleteBonusProgramRegistration.vue, apps/storefront-telegram/server/api/auth/bonus.post.ts, apps/storefront-telegram/shared/services/client.ts, packages/database/src/repository/client.ts, packages/database/src/tables.ts, apps/storefront-telegram/app/stores/client.ts, apps/storefront-telegram/i18n/locales/ru-RU.json, apps/storefront-telegram/app/components/client/LevelIncreaseProgress.vue, apps/storefront-telegram/app/components/client/PointsCardBottomBlock.vue, apps/storefront-telegram/app/components/client/PointsCard.vue
Implemented end-to-end bonus registration: new UI (button, drawer, form), validation schema/type, API endpoint, repo update method, DB columns (email, birthDate, bonusProgramParticipantFrom), store fields/computed, i18n labels, level progress/bottom block components. Integrated into PointsCard and tweaked drawer/header text.
Gyroscope composable + refactor
apps/storefront-telegram/app/composables/useGyroscope.ts, apps/storefront-telegram/app/components/client/PointsCard.vue
Added shared gyroscope composable; PointsCard now uses it, removing manual listeners.
Initialization guards
apps/storefront-telegram/app/utils/init.ts
Switched to per-feature .ifAvailable() for SDK mounts; added mountSwipeBehavior.ifAvailable().
Tooling
pnpm-workspace.yaml
Bumped TypeScript catalog from ^5.8.3 to ^5.9.2.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant U as User
  participant C as Client UI (Points/Registration)
  participant F as Form (CompleteBonusProgramRegistration)
  participant A as API /api/auth/bonus
  participant R as Repo Client.updateBonusProgram
  participant S as Store (useClientStore)
  participant CF as Confetti (useConfetti)

  U->>C: Tap "Активировать"
  C->>C: Open drawer
  U->>F: Submit form (name, surname?, email, birthDate)
  F->>A: POST body + auth
  A->>R: updateBonusProgram(id, data)
  R-->>A: Updated client
  A-->>F: { ok: true }
  F->>S: S.update()
  F->>CF: pop()
  F->>C: emit success / close drawer

  rect rgba(200,255,200,0.2)
  note right of CF: New shared confetti shown\nfor ~5s overlay
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • chore: visual updates #128 — Also touches gyroscope handling; this PR centralizes it via a shared composable and refactors usage accordingly.

Poem

A rabbit taps “Активировать” with glee,
Confetti blooms—oh what a sight to see! 🎉
New routes hop to client’s den,
Bonuses bloom, then bloom again.
Gyros spin, the drawer glides—
I thump approval, whiskers wide. 🐇

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch bonus-program

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented Sep 5, 2025

@hmbanan666 hmbanan666 merged commit 3f38d52 into main Sep 5, 2025
7 of 8 checks passed
@hmbanan666 hmbanan666 deleted the bonus-program branch September 5, 2025 09:51
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/storefront-telegram/app/components/Navigation.vue (1)

35-39: Update stale route in atrium-telegram and localize all titles

  • In apps/storefront-telegram/app/components/Navigation.vue (lines 31 & 37), replace hard-coded titles with i18n calls:
    -    title: 'Кабинет',
    +    title: t('nav.cabinet'),
    -    title: 'Меню',
    +    title: t('nav.menu'),
  • In apps/atrium-telegram/app/components/Navigation.vue, the route still uses path: '/secret1' (line 53) and title: 'Секрет'; rename the path to /navigation and replace the hard-coded title with t('nav.secret').
apps/storefront-telegram/app/components/client/PointsCard.vue (1)

107-114: Add rel to external links opened in a new tab.

Prevent window.opener leaks.

-          <ULink to="https://sushi-love.ru" target="_blank">
+          <ULink to="https://sushi-love.ru" target="_blank" rel="noopener noreferrer">
             условиями программы лояльности,
           </ULink>
-          <ULink to="https://sushi-love.ru" target="_blank">
+          <ULink to="https://sushi-love.ru" target="_blank" rel="noopener noreferrer">
             политикой конфиденциальности
           </ULink> и <ULink to="https://sushi-love.ru" target="_blank">
             условиями обработки персональных данных.
           </ULink>
🧹 Nitpick comments (22)
apps/storefront-telegram/app/composables/useConfetti.ts (1)

1-15: Make pops idempotent and clear the timeout on scope dispose.

Prevents multiple timers from fighting and cleans up when scope ends.

Apply:

 function _useConfetti() {
   const isShown = ref(false)
+  let t: ReturnType<typeof setTimeout> | undefined

-  function pop() {
-    isShown.value = true
-    setTimeout(() => {
-      isShown.value = false
-    }, 5000)
-  }
+  function pop(durationMs = 5000) {
+    if (t) clearTimeout(t)
+    isShown.value = true
+    t = setTimeout(() => {
+      isShown.value = false
+      t = undefined
+    }, durationMs)
+  }
+
+  tryOnScopeDispose(() => {
+    if (t) clearTimeout(t)
+  })

   return { isShown, pop }
 }
apps/storefront-telegram/i18n/locales/ru-RU.json (1)

9-15: Localize “email” and confirm new keys exist in all locales

  • Suggest translating “Email” to “Электронная почта” for consistency with Russian UI text.
  • Please verify that common.surname, common.email, and common.birth-date are added to other locales to avoid runtime fallbacks.

Apply:

-    "email": "Email",
+    "email": "Электронная почта",
apps/storefront-telegram/app/composables/useGyroscope.ts (1)

1-3: Add explicit imports for Vue/VueUse utilities to prevent build-time issues

If auto-imports are not enabled for TS files, ref, onMounted, onUnmounted, and createSharedComposable will be undefined at compile time. Add explicit imports.

 import type { EventListener } from '@telegram-apps/sdk-vue'
 import { off, on } from '@telegram-apps/sdk-vue'
+import { ref, onMounted, onUnmounted } from 'vue'
+import { createSharedComposable } from '@vueuse/core'
packages/database/src/tables.ts (1)

604-611: Add basic data integrity for new client fields (email/birthDate) and confirm uniqueness strategy

  • Consider enforcing a reasonable birth_date range (not in the future, >= a sane minimum like 1900-01-01) and validating email format server-side (and/or add a functional unique index on lower(email) if you want per-client uniqueness, case-insensitive).
  • If duplicates are intentionally allowed across clients, ignore the unique index suggestion but please confirm.
apps/storefront-telegram/app/components/NavigationButton.vue (1)

53-55: Avoid history stack bloat when returning from nested client pages.

Consider using router.replace('/client') when canReturn is true so Back won’t take users back into the nested page.

Example change outside this hunk:

- function handleRedirect(path: string) {
-  vibrate()
-  router.push(path)
- }
+ function handleRedirect(path: string, { replace = false } = {}) {
+  vibrate()
+  return replace ? router.replace(path) : router.push(path)
+}

Then in the template:

- @click="canScrollToTop ? handleScrollToTop() : handleRedirect(route.path)"
+ @click="canScrollToTop ? handleScrollToTop() : handleRedirect(canReturn ? '/client' : route.path, { replace: canReturn })"
apps/storefront-telegram/app/layouts/default.vue (1)

2-2: Confirm confetti overlay doesn’t block UI interactions.

Ensure the root of BackgroundConfetti has pointer-events-none (or equivalent) and an appropriate z-index so bottom navigation and city selector remain clickable.

I can review components/background/Confetti.vue and propose a minimal diff if needed.

apps/storefront-telegram/app/pages/navigation.vue (1)

1-5: Localize the placeholder heading.

Hardcoded Russian text bypasses i18n. Use t() to enable translations.

-  <PageContainer>
-    <h1>Еще рано сюда заходить</h1>
-  </PageContainer>
+  <PageContainer>
+    <h1>{{ $t('navigation.comingSoon') }}</h1>
+  </PageContainer>

If the key doesn’t exist, I can add it to locales.

apps/storefront-telegram/app/components/background/Confetti.vue (1)

3-3: Avoid hardcoded stage dimensions; let it fit the viewport.

Static 900×1200 can clip on many devices. Either omit stageWidth/Height (if the directive auto-sizes) or bind them reactively to the window size on client.

Example minimal change if auto-size is supported:

-    <div v-confetti="{ particleCount: 240, duration: 4500, stageHeight: 1200, stageWidth: 900, force: 0.4 }" />
+    <div v-confetti="{ particleCount: 240, duration: 4500, force: 0.4 }" />
apps/storefront-telegram/app/components/client/LevelIncreaseProgress.vue (2)

8-9: Use proper currency formatting.

Concatenating a sign can misformat spacing/decimals. Prefer Intl currency format (using a currency code from the channel store, if available).

-        Закажите еще на {{ Intl.NumberFormat('ru').format(clientStore.nextLevelAmount) }} {{ channelStore.currencySign }}
+        Закажите еще на
+        {{
+          Intl.NumberFormat('ru-RU', { style: 'currency', currency: channelStore.currencyCode || 'RUB' })
+            .format(clientStore.nextLevelAmount)
+        }}

5-9: Externalize user-facing strings to i18n.

Move hardcoded RU strings to locale files for consistency with the rest of the app.

apps/storefront-telegram/server/api/auth/bonus.post.ts (2)

1-11: Email and birthDate validation appear too lax upstream.

The shared schema snippet validates only string length; add an email pattern and ISO date check to prevent bad data.

Follow-up in apps/storefront-telegram/shared/services/client.ts (not in this diff):

export const completeBonusProgramRegistrationSchema = type({
  name: type('2 <= string <= 50').describe('error.length.invalid'),
  surname: type('2 <= string <= 50 | undefined').optional(),
  email: type('email & string <= 80').describe('error.email.invalid'),
  birthDate: type('string & /^(\\d{4})-(\\d{2})-(\\d{2})$/').describe('error.date.invalid'),
})

(Adjust for arktype’s exact syntax you use.)


1-23: Log hygiene for PII.

Ensure any centralized errorResolver or request logging excludes name/email/birthDate from logs.

apps/storefront-telegram/app/components/client/BonusProgramRegistration.vue (3)

80-83: Open the drawer deterministically.

Avoid toggling to prevent accidental close on rapid taps.

-function handleClick() {
-  vibrate()
-  isDrawerOpened.value = !isDrawerOpened.value
-}
+function handleClick() {
+  vibrate()
+  isDrawerOpened.value = true
+}

59-69: External links: add rel for safety.

For target="_blank", include rel="noopener noreferrer".

-          <ULink to="https://sushi-love.ru" target="_blank">
+          <ULink to="https://sushi-love.ru" target="_blank" rel="noopener noreferrer">
...
-          <ULink to="https://sushi-love.ru" target="_blank">
+          <ULink to="https://sushi-love.ru" target="_blank" rel="noopener noreferrer">
...
-          </ULink> и <ULink to="https://sushi-love.ru" target="_blank">
+          </ULink> и <ULink to="https://sushi-love.ru" target="_blank" rel="noopener noreferrer">

4-10: Localize strings.

Move RU strings to i18n for consistency.

apps/storefront-telegram/app/pages/client/index.vue (1)

4-9: Localize header/labels.

Move hardcoded RU strings to i18n.

apps/storefront-telegram/app/components/form/CompleteBonusProgramRegistration.vue (3)

31-41: Use proper email input semantics.

Improve UX/validation on mobile by using type="email" and autocomplete.

   <UInput
     v-model="state.email"
+    type="email"
+    inputmode="email"
+    autocomplete="email"
     size="xl"
     class="w-full"
   />

48-54: Date input: add autocomplete.

Small UX boost for some browsers.

   <UInput
     v-model="state.birthDate"
     type="date"
+    autocomplete="bday"
     size="xl"
     class="w-full"
   />

63-64: Localize the submit label.

Use $t(...) for consistency with other labels.

apps/storefront-telegram/app/components/client/PointsCard.vue (3)

54-55: Lazy-load registration/progress components.

Reduce initial payload with Nuxt’s lazy components.

-    <ClientBonusProgramRegistration v-if="!clientStore.isBonusProgramParticipant" />
-    <ClientLevelIncreaseProgress v-else />
+    <LazyClientBonusProgramRegistration v-if="!clientStore.isBonusProgramParticipant" />
+    <LazyClientLevelIncreaseProgress v-else />

7-8: Clamp gyro rotation to avoid extreme transforms.

A noisy sensor can yield jerky rotations. Clamp in style or inside useGyroscope().

Also applies to: 14-15


86-88: Copy nit: “всё время”.

Minor Russian grammar tweak: “за всё время”.

-            Кешбэк определяется по сумме заказов за все время. Система включает уровни
+            Кешбэк определяется по сумме заказов за всё время. Система включает уровни
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 1b2a04f and b92cc8a.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (24)
  • apps/storefront-telegram/app/app.config.ts (1 hunks)
  • apps/storefront-telegram/app/components/CitySelector.vue (0 hunks)
  • apps/storefront-telegram/app/components/Navigation.vue (1 hunks)
  • apps/storefront-telegram/app/components/NavigationButton.vue (2 hunks)
  • apps/storefront-telegram/app/components/background/Confetti.vue (1 hunks)
  • apps/storefront-telegram/app/components/client/BonusProgramRegistration.vue (1 hunks)
  • apps/storefront-telegram/app/components/client/LevelIncreaseProgress.vue (1 hunks)
  • apps/storefront-telegram/app/components/client/PointsCard.vue (3 hunks)
  • apps/storefront-telegram/app/components/client/PointsCardBottomBlock.vue (1 hunks)
  • apps/storefront-telegram/app/components/form/CompleteBonusProgramRegistration.vue (1 hunks)
  • apps/storefront-telegram/app/composables/useConfetti.ts (1 hunks)
  • apps/storefront-telegram/app/composables/useGyroscope.ts (1 hunks)
  • apps/storefront-telegram/app/layouts/default.vue (1 hunks)
  • apps/storefront-telegram/app/pages/client/index.vue (1 hunks)
  • apps/storefront-telegram/app/pages/navigation.vue (1 hunks)
  • apps/storefront-telegram/app/pages/user.vue (0 hunks)
  • apps/storefront-telegram/app/stores/client.ts (4 hunks)
  • apps/storefront-telegram/app/utils/init.ts (2 hunks)
  • apps/storefront-telegram/i18n/locales/ru-RU.json (1 hunks)
  • apps/storefront-telegram/server/api/auth/bonus.post.ts (1 hunks)
  • apps/storefront-telegram/shared/services/client.ts (1 hunks)
  • packages/database/src/repository/client.ts (1 hunks)
  • packages/database/src/tables.ts (1 hunks)
  • pnpm-workspace.yaml (1 hunks)
💤 Files with no reviewable changes (2)
  • apps/storefront-telegram/app/pages/user.vue
  • apps/storefront-telegram/app/components/CitySelector.vue
🧰 Additional context used
🧬 Code graph analysis (2)
packages/database/src/repository/client.ts (2)
packages/database/src/types.ts (1)
  • ClientDraft (146-146)
packages/database/src/tables.ts (1)
  • clients (599-617)
apps/storefront-telegram/server/api/auth/bonus.post.ts (1)
apps/storefront-telegram/shared/services/client.ts (1)
  • completeBonusProgramRegistrationSchema (8-13)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (14)
pnpm-workspace.yaml (1)

69-69: No TS pin conflicts detected—confirm toolchain compatibility. All packages now reference TypeScript ^5.9.2 via the workspace catalog with no local overrides. Run a full CI typecheck/build to ensure Nuxt 4, @vitejs/plugin-vue 6, and vue-tsc 3 all remain green.

apps/storefront-telegram/app/app.config.ts (1)

54-58: Use a valid Tailwind arbitrary‐value utility for drawer max-height
max-h-10/12 isn’t provided by default and will fail to compile. Replace it with an arbitrary‐value class, for example:

-        content: '!max-h-10/12',
+        // Cap drawer content height to avoid viewport overflow on mobile
+        content: '!max-h-[80vh]',

Also verify that content is the correct slot name for UDrawer in your installed @nuxt/ui version.

apps/storefront-telegram/app/components/client/PointsCardBottomBlock.vue (1)

1-7: LGTM: clean slot wrapper.

Simple, reusable container with sensible defaults.

apps/storefront-telegram/app/utils/init.ts (1)

14-15: Granular feature gating looks good; double-check gyroscope availability/rate

  • Nice move to per-feature .ifAvailable() checks and adding mountSwipeBehavior.
  • Consider confirming that web_app_start_gyroscope is supported on your minimum Telegram client versions and that refresh_rate: 80 is acceptable; some environments throttle or cap rates.

Also applies to: 80-85, 87-89, 95-98

apps/storefront-telegram/app/components/NavigationButton.vue (2)

17-21: Good UX: return affordance on nested client routes.

The conditional undo icon correctly signals “return to /client” when nested. Click behavior routes to route.path anyway, so the UX aligns.


50-52: Safer scroll-to-top gating.

Requiring exact '/' for both button and current route prevents false positives. Looks good.

apps/storefront-telegram/app/stores/client.ts (3)

23-24: New bonus-program flags wired correctly.

bonusProgramParticipantFrom + isBonusProgramParticipant are straightforward and composable-friendly.


85-85: API hydration includes bonus flag.

Assigning bonusProgramParticipantFrom from /api/auth/me response is correct.


217-219: Store exposure is consistent.

Exposing the new refs keeps the public API cohesive.

apps/storefront-telegram/app/components/Navigation.vue (1)

29-31: No remaining references to the old ‘user’ route — project-wide search across .vue/.js/.ts for name: 'user', path: '/user', to="/user", and router.push|replace({ name: 'user' }) returned zero matches.

apps/storefront-telegram/app/components/background/Confetti.vue (1)

2-2: z-index summary: Confetti.vue uses z-40, the same as Modal.vue, and sits beneath Navigation.vue’s z-50.
If confetti must appear above modals or the bottom nav, update to z-50 (or extend Tailwind for a higher value like z-[60]).

apps/storefront-telegram/app/components/client/LevelIncreaseProgress.vue (1)

2-2: Check 0-threshold case.

v-if hides the block when nextLevelAmount is 0. If “exactly at threshold” should still show the progress (or a different state), handle 0 explicitly.

apps/storefront-telegram/app/pages/client/index.vue (1)

47-50: Revisit the “Select city” action.

Calling updateCity(null) clears the city rather than opening selection. If there’s a CitySelector flow, hook into it (e.g., open a drawer or navigate).

apps/storefront-telegram/app/components/client/PointsCard.vue (1)

123-123: Confirm SSR-safety of useGyroscope.

Ensure the composable guards for process.client/onMounted and degrades gracefully when sensors are unavailable.

Comment on lines +2 to +4
<div v-if="isShown" class="z-40 mx-auto h-dvh w-full fixed inset-0 overflow-y-hidden overscroll-y-none">
<div v-confetti="{ particleCount: 240, duration: 4500, stageHeight: 1200, stageWidth: 900, force: 0.4 }" />
</div>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Don’t block interactions; make overlay non-interactive and hidden from a11y.

Add pointer-events-none and aria-hidden so the confetti layer doesn’t steal taps/clicks and is ignored by screen readers.

Apply:

-  <div v-if="isShown" class="z-40 mx-auto h-dvh w-full fixed inset-0 overflow-y-hidden overscroll-y-none">
+  <div
+    v-if="isShown"
+    class="z-40 pointer-events-none mx-auto h-dvh w-full fixed inset-0 overflow-y-hidden overscroll-y-none"
+    aria-hidden="true"
+  >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div v-if="isShown" class="z-40 mx-auto h-dvh w-full fixed inset-0 overflow-y-hidden overscroll-y-none">
<div v-confetti="{ particleCount: 240, duration: 4500, stageHeight: 1200, stageWidth: 900, force: 0.4 }" />
</div>
<div
v-if="isShown"
class="z-40 pointer-events-none mx-auto h-dvh w-full fixed inset-0 overflow-y-hidden overscroll-y-none"
aria-hidden="true"
>
<div
v-confetti="{ particleCount: 240, duration: 4500, stageHeight: 1200, stageWidth: 900, force: 0.4 }"
/>
</div>
🤖 Prompt for AI Agents
In apps/storefront-telegram/app/components/background/Confetti.vue around lines
2-4 the confetti overlay currently blocks user interactions and is announced to
assistive tech; add the utility class pointer-events-none to the outer div and
set aria-hidden="true" (and optionally role="presentation") on the same element
so the layer is non-interactive and hidden from screen readers while preserving
visual effect.

Comment on lines +55 to +57
<FormCompleteBonusProgramRegistration
@submitted="handleClose()"
/>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Close the drawer only on successful submission.

If submitted fires before success, the drawer may close on failures. Prefer a success/completed event.

-        <FormCompleteBonusProgramRegistration
-          @submitted="handleClose()"
-        />
+        <FormCompleteBonusProgramRegistration
+          @success="handleClose()"
+        />

(If the child emits only submitted, emit success there upon API ok.)

🤖 Prompt for AI Agents
In apps/storefront-telegram/app/components/client/BonusProgramRegistration.vue
around lines 55-57, the parent currently closes the drawer on the child's
"submitted" event which may fire before the operation succeeds; change the
parent to close the drawer only when the child emits a success/completed event
(e.g., "success" or "completed") and keep the child emitting "submitted" for
attempt and emit the new success event only after the API call returns OK; if
the child does not yet emit such an event, update the child to emit a
success/completed event on successful API response and update the parent to
listen for that event instead of "submitted".

Comment on lines +13 to +18
v-model="clientStore.nextLevelProgressPercent"
color="primary"
:ui="{
base: 'bg-primary/10',
}"
/>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Don’t use v-model for a read-only progress bar.

Bind the value one-way to avoid accidental store mutations and make intent clear.

-    <UProgress
-      v-model="clientStore.nextLevelProgressPercent"
+    <UProgress
+      :model-value="clientStore.nextLevelProgressPercent"
       color="primary"
       :ui="{
         base: 'bg-primary/10',
       }"
     />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
v-model="clientStore.nextLevelProgressPercent"
color="primary"
:ui="{
base: 'bg-primary/10',
}"
/>
<UProgress
:model-value="clientStore.nextLevelProgressPercent"
color="primary"
:ui="{
base: 'bg-primary/10',
}"
/>
🤖 Prompt for AI Agents
In apps/storefront-telegram/app/components/client/LevelIncreaseProgress.vue
around lines 13–18, the progress bar uses v-model which two-way binds to
clientStore.nextLevelProgressPercent; change this to a one-way binding to avoid
accidental store mutations — replace v-model with a read-only prop (e.g.,
:value="clientStore.nextLevelProgressPercent" or
:model-value="clientStore.nextLevelProgressPercent" depending on the progress
component API) and remove any accompanying @update or input handlers so the
component only reads the store value.

Comment on lines +3 to +4
:validate="createValidator(completeBonusProgramRegistrationSchema)"
:state="state"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

❓ Verification inconclusive

Import createValidator (avoid runtime undefined).

Unless auto-imported, createValidator will be undefined in the template. Import it explicitly.

Apply:

 <script setup lang="ts">
 import type { CompleteBonusProgramRegistration } from '#shared/services/client'
 import type { FormSubmitEvent } from '@nuxt/ui'
+import { createValidator } from '@nuxt/ui'
 import { completeBonusProgramRegistrationSchema } from '#shared/services/client'

Also applies to: 72-76


Import createValidator from @nuxt/ui to avoid runtime undefined.
Add alongside the existing imports in <script setup> (e.g. import { createValidator } from '@nuxt/ui'), and repeat for the identical usage at lines 72–76.

🤖 Prompt for AI Agents
In
apps/storefront-telegram/app/components/form/CompleteBonusProgramRegistration.vue
around lines 3–4 and again where an identical usage appears at lines 72–76, the
template uses createValidator but it is not imported which will cause a runtime
undefined; add an import for createValidator from '@nuxt/ui' in the <script
setup> imports (e.g. import { createValidator } from '@nuxt/ui') so the function
is available for both template bindings.

Comment on lines +56 to +68
<UButton
type="submit"
variant="solid"
color="secondary"
size="xl"
trailing-icon="i-lucide-flag"
block
label="Завершить регистрацию"
class="mt-3"
:ui="{
trailingIcon: 'ms-0',
}"
/>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Prevent double-submit and handle missing auth defensively.

Add a pending flag to disable the button and guard when initDataRaw is absent. Also remove unnecessary Promise.all.

@@
-    <UButton
+    <UButton
       type="submit"
       variant="solid"
       color="secondary"
       size="xl"
       trailing-icon="i-lucide-flag"
       block
       label="Завершить регистрацию"
       class="mt-3"
+      :loading="pending"
       :ui="{
         trailingIcon: 'ms-0',
       }"
     />
@@
-const state = ref<Partial<CompleteBonusProgramRegistration>>({
+const state = ref<Partial<CompleteBonusProgramRegistration>>({
   name: clientStore.name,
   surname: clientStore.surname,
   email: undefined,
   birthDate: undefined,
 })
+const pending = ref(false)
@@
 async function onSubmit(event: FormSubmitEvent<CompleteBonusProgramRegistration>) {
   emit('submitted')
 
   try {
+    if (!clientStore.initDataRaw) {
+      vibrate('error')
+      return
+    }
+    pending.value = true
     await $fetch('/api/auth/bonus', {
       method: 'POST',
       headers: {
         Authorization: `tma ${clientStore.initDataRaw}`,
       },
       body: event.data,
     })
 
-    await Promise.all([
-      clientStore.update(),
-    ])
+    await clientStore.update()
 
     vibrate('success')
     pop()
     emit('success')
   } catch (error) {
     console.error(error)
     vibrate('error')
+  } finally {
+    pending.value = false
   }
 }

Also applies to: 83-88, 90-113

🤖 Prompt for AI Agents
In
apps/storefront-telegram/app/components/form/CompleteBonusProgramRegistration.vue
around lines 56-68 (also apply same changes to 83-88 and 90-113), prevent
double-submit and handle missing auth defensively: add a reactive pending flag
(e.g., ref(false)) and bind it to the UButton disabled prop (or use
:disabled="pending") so the button is disabled while submission is in progress;
at the start of the submit handler return early if pending is true or if
initDataRaw is falsy (guard against missing auth/data); set pending = true
before async work and ensure pending = false in a finally block; replace the
unnecessary Promise.all wrapper with direct awaits for sequential/independent
calls as appropriate to your logic, and ensure any error paths log/handle errors
while still clearing pending.

Comment on lines +7 to +11
const body = await readBody(event)
const data = completeBonusProgramRegistrationSchema(body)
if (data instanceof type.errors) {
throw data
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Strengthen validation flow with explicit guards.

Ensure body is an object and surface validation errors with a 400 code. Current arktype instanceof check may be brittle across versions.

-    const body = await readBody(event)
-    const data = completeBonusProgramRegistrationSchema(body)
-    if (data instanceof type.errors) {
-      throw data
-    }
+    const body = await readBody(event)
+    if (!body || typeof body !== 'object') {
+      throw createError({ statusCode: 400, statusMessage: 'Invalid body' })
+    }
+    const data = completeBonusProgramRegistrationSchema(body)
+    if (data instanceof type.errors) {
+      throw createError({ statusCode: 400, statusMessage: 'Validation error', data })
+    }

(If you keep errorResolver, pass data into it.)

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const body = await readBody(event)
const data = completeBonusProgramRegistrationSchema(body)
if (data instanceof type.errors) {
throw data
}
const body = await readBody(event)
if (!body || typeof body !== 'object') {
throw createError({ statusCode: 400, statusMessage: 'Invalid body' })
}
const data = completeBonusProgramRegistrationSchema(body)
if (data instanceof type.errors) {
throw createError({ statusCode: 400, statusMessage: 'Validation error', data })
}
🤖 Prompt for AI Agents
In apps/storefront-telegram/server/api/auth/bonus.post.ts around lines 7 to 11,
the current validation uses an instanceof check on arktype errors which is
brittle and doesn't ensure the request body is an object or return a proper 400;
update the flow to first assert that body is a non-null object before calling
completeBonusProgramRegistrationSchema, then handle validation failures by
detecting the schema error shape (avoid instanceof) and throw a HTTP 400 error
with the validation details (or call errorResolver with the validation result
passed in if you keep errorResolver). Ensure the thrown/returned error includes
a statusCode 400 and the schema error payload so clients receive clear
validation feedback.

Comment on lines +13 to +15
await repository.client.updateBonusProgram(event.context.client.id, {
...data,
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add an auth guard for event.context.client.

Defensive check avoids crashes when context is missing and returns 401.

-    await repository.client.updateBonusProgram(event.context.client.id, {
+    const clientId = event.context.client?.id
+    if (!clientId) {
+      throw createError({ statusCode: 401, statusMessage: 'Unauthorized' })
+    }
+    await repository.client.updateBonusProgram(clientId, {
       ...data,
     })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await repository.client.updateBonusProgram(event.context.client.id, {
...data,
})
const clientId = event.context.client?.id
if (!clientId) {
throw createError({ statusCode: 401, statusMessage: 'Unauthorized' })
}
await repository.client.updateBonusProgram(clientId, {
...data,
})
🤖 Prompt for AI Agents
In apps/storefront-telegram/server/api/auth/bonus.post.ts around lines 13–15,
add a defensive auth guard that checks event.context.client before calling
repository.client.updateBonusProgram; if event.context.client is missing or
falsy, return an HTTP 401 Unauthorized response (early return) instead of
proceeding, e.g. perform the guard at the top of the handler and use the
project's standard error/response helper to send a 401 and stop execution.

Comment on lines +21 to +22
throw errorResolver(error)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Missing import for errorResolver causes runtime/compile failure.

errorResolver is undefined here. Either import it or use createError/sendError.

Option A — import your helper (adjust path as in your project):

-import { completeBonusProgramRegistrationSchema } from '#shared/services/client'
+import { completeBonusProgramRegistrationSchema } from '#shared/services/client'
+import { errorResolver } from '#server/utils/errorResolver' // <-- verify path

Option B — replace with h3 createError:

-  } catch (error) {
-    throw errorResolver(error)
-  }
+  } catch (error) {
+    throw createError({ statusCode: 400, statusMessage: 'Bad Request', data: error })
+  }

If using Option B, add:

+import { createError } from 'h3'
🤖 Prompt for AI Agents
In apps/storefront-telegram/server/api/auth/bonus.post.ts around lines 21-22,
errorResolver is referenced but not imported causing a compile/runtime failure;
fix by either importing errorResolver from the correct helper path used in the
project (add the proper import statement at the top of the file) or replace the
call with a framework error utility (e.g., use h3's createError or the project
sendError helper), making sure to return or throw the created error consistently
with surrounding handlers.

Comment on lines +8 to +14
export const completeBonusProgramRegistrationSchema = type({
name: type('2 <= string <= 50').describe('error.length.invalid'),
surname: type('2 <= string <= 50 | undefined').describe('error.length.invalid').optional(),
email: type('2 <= string <= 80').describe('error.length.invalid'),
birthDate: type('string').describe('error.length.invalid'),
})
export type CompleteBonusProgramRegistration = typeof completeBonusProgramRegistrationSchema.infer
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Strengthen server validation for email and birthDate

Use ArkType’s built-in predicates for email and ISO date instead of plain strings/length checks.

References: ArkType string predicates (e.g., string.email, string.date.iso). (arktype.io)

 export const completeBonusProgramRegistrationSchema = type({
   name: type('2 <= string <= 50').describe('error.length.invalid'),
   surname: type('2 <= string <= 50 | undefined').describe('error.length.invalid').optional(),
-  email: type('2 <= string <= 80').describe('error.length.invalid'),
-  birthDate: type('string').describe('error.length.invalid'),
+  email: type('2 <= string.email <= 80').describe('error.length.invalid'),
+  birthDate: type('string.date.iso').describe('error.length.invalid'),
 })

Optionally, add a “not in the future” guard for birthDate with a refinement if needed.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const completeBonusProgramRegistrationSchema = type({
name: type('2 <= string <= 50').describe('error.length.invalid'),
surname: type('2 <= string <= 50 | undefined').describe('error.length.invalid').optional(),
email: type('2 <= string <= 80').describe('error.length.invalid'),
birthDate: type('string').describe('error.length.invalid'),
})
export type CompleteBonusProgramRegistration = typeof completeBonusProgramRegistrationSchema.infer
export const completeBonusProgramRegistrationSchema = type({
name: type('2 <= string <= 50').describe('error.length.invalid'),
surname: type('2 <= string <= 50 | undefined').describe('error.length.invalid').optional(),
email: type('2 <= string.email <= 80').describe('error.length.invalid'),
birthDate: type('string.date.iso').describe('error.length.invalid'),
})
export type CompleteBonusProgramRegistration = typeof completeBonusProgramRegistrationSchema.infer
🤖 Prompt for AI Agents
In apps/storefront-telegram/shared/services/client.ts around lines 8 to 14, the
schema currently uses generic string/length checks for email and birthDate;
replace those with ArkType string predicates: use string.email for the email
field and string.date.iso for birthDate (and update error messages if needed).
Ensure the surname remains optional with its length predicate. Optionally add a
refinement/guard on birthDate to reject ISO dates in the future (e.g., compare
parsed date to now) and return a clear validation message. Also confirm any
necessary imports from ArkType (string/date predicates) are present at the top
of the file.

Comment on lines +78 to +89
static async updateBonusProgram(id: string, data: Pick<ClientDraft, 'name' | 'surname' | 'email' | 'birthDate'>) {
const [client] = await useDatabase()
.update(clients)
.set({
...data,
bonusProgramParticipantFrom: sql`now()`,
updatedAt: sql`now()`,
})
.where(eq(clients.id, id))
.returning()
return client
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

Preserve first participation date and avoid overwriting columns with undefined.

  • Overwriting bonusProgramParticipantFrom with now() on every edit loses the original join date.
  • Spreading ...data may push undefined into nullable/non-nullable columns depending on Zod parsing; filter out undefined keys.

Use COALESCE and filter undefined:

   static async updateBonusProgram(id: string, data: Pick<ClientDraft, 'name' | 'surname' | 'email' | 'birthDate'>) {
-    const [client] = await useDatabase()
-      .update(clients)
-      .set({
-        ...data,
-        bonusProgramParticipantFrom: sql`now()`,
-        updatedAt: sql`now()`,
-      })
+    // Only persist provided fields; do not clobber with undefined.
+    const patch = Object.fromEntries(
+      Object.entries(data).filter(([, v]) => v !== undefined)
+    ) as Partial<ClientDraft>
+
+    const [client] = await useDatabase()
+      .update(clients)
+      .set({
+        ...patch,
+        // Keep the original participation timestamp if already set
+        bonusProgramParticipantFrom: sql`coalesce(${clients.bonusProgramParticipantFrom}, now())`,
+        updatedAt: sql`now()`,
+      })
       .where(eq(clients.id, id))
       .returning()
     return client
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
static async updateBonusProgram(id: string, data: Pick<ClientDraft, 'name' | 'surname' | 'email' | 'birthDate'>) {
const [client] = await useDatabase()
.update(clients)
.set({
...data,
bonusProgramParticipantFrom: sql`now()`,
updatedAt: sql`now()`,
})
.where(eq(clients.id, id))
.returning()
return client
}
static async updateBonusProgram(
id: string,
data: Pick<ClientDraft, 'name' | 'surname' | 'email' | 'birthDate'>
) {
// Only persist provided fields; do not clobber with undefined.
const patch = Object.fromEntries(
Object.entries(data).filter(([, v]) => v !== undefined)
) as Partial<ClientDraft>
const [client] = await useDatabase()
.update(clients)
.set({
...patch,
// Keep the original participation timestamp if already set
bonusProgramParticipantFrom: sql`
coalesce(${clients.bonusProgramParticipantFrom}, now())
`,
updatedAt: sql`now()`,
})
.where(eq(clients.id, id))
.returning()
return client
}
🤖 Prompt for AI Agents
In packages/database/src/repository/client.ts around lines 78 to 89, the update
currently overwrites bonusProgramParticipantFrom with now() on every edit and
spreads possibly-undefined values from data; change the update to (1) filter the
incoming data to remove keys with undefined so you only set provided fields, (2)
set bonusProgramParticipantFrom using a COALESCE-style expression so it keeps
the existing value if present and only sets now() when null/undefined, and (3)
always set updatedAt to now(); ensure the .set call only includes the filtered
fields plus the COALESCE expression and updatedAt, then return the updated
client as before.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant