feat: feedback service - #95
Conversation
WalkthroughIntroduces a shared haptic feedback composable and migrates components to use it for vibration on interactions. Updates UI details in several components, adjusts borders and tooltips, adds a CSS root variable for muted text, modifies the index page avatar flow, and extends Russian locale strings. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant C as UI Component
participant F as useFeedback()
participant H as hapticFeedback API
rect rgba(230,245,255,0.6)
note right of C: On user interaction (click/change)
User->>C: Trigger event
C->>F: vibrate()
alt haptic available
F->>H: impactOccurred('light')
H-->>F: ack
else unavailable
F-->>C: no-op
end
end
note over C: Continue normal flow (e.g., open modal, navigate)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
|
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/atrium-telegram/app/components/TaskCard.vue (1)
144-164: Type error: DropdownMenuItem doesn’t contain “condition”; build will fail under strict TSBuild the array conditionally instead of attaching a non-existent property.
-const items = computed<DropdownMenuItem[]>(() => { - const menuItems: DropdownMenuItem[] = [ - { - label: isFocused.value ? 'Убрать фокус' : 'Сфокусироваться', - icon: 'i-lucide-goal', - color: 'neutral', - disabled: false, - onSelect: isFocused.value ? onUnfocus : onFocus, - condition: canFocus.value, - }, - { - label: 'Редактировать', - icon: 'i-lucide-edit', - disabled: isCompleted.value, - onSelect: () => modalUpdateTask.open({ taskId: task.id }), - condition: canEdit.value, - }, - ] - - return menuItems.filter((item) => item.condition) -}) +const items = computed<DropdownMenuItem[]>(() => { + const menuItems: DropdownMenuItem[] = [] + if (canFocus.value) { + menuItems.push({ + label: isFocused.value ? 'Убрать фокус' : 'Сфокусироваться', + icon: 'i-lucide-goal', + color: 'neutral', + disabled: false, + onSelect: isFocused.value ? onUnfocus : onFocus, + }) + } + if (canEdit.value) { + menuItems.push({ + label: 'Редактировать', + icon: 'i-lucide-edit', + disabled: isCompleted.value, + onSelect: () => modalUpdateTask.open({ taskId: task.id }), + }) + } + return menuItems +})
🧹 Nitpick comments (8)
apps/atrium-telegram/app/assets/css/styles.css (1)
6-6: Good addition; consider a safe fallback.
Add a fallback to guard against missing theme vars in non-Telegram contexts.- --ui-text-muted: var(--tg-theme-hint-color); + --ui-text-muted: var(--tg-theme-hint-color, #999999);apps/atrium-telegram/app/components/TasksTodaySwitch.vue (1)
12-12: Confirm the emitted event name for USwitch.
If USwitch doesn’t emit a native change, prefer the model update event.- @change="vibrate" + @update:model-value="vibrate"apps/atrium-telegram/app/components/Navigation.vue (1)
65-68: Avoid redundant navigations.
Skip router.push when already on the target path.function handleClick(path: string) { vibrate() - router.push(path) + if (router.currentRoute.value.path !== path) { + router.push(path) + } }apps/atrium-telegram/app/components/Button.vue (1)
2-5: Set an explicit button type.
Prevents accidental form submission when used inside forms.- <button + <button + type="button" class="p-3 tg-bg-button w-full rounded-2xl font-medium cursor-pointer active:scale-95 duration-200" @click="vibrate" >apps/atrium-telegram/app/components/CreateCard.vue (1)
10-12: Avoid recreating inline objects on each renderHoist the ui object to a const (as shown above) to reduce churn and improve readability.
apps/atrium-telegram/app/composables/useFeedback.ts (1)
4-8: Harden for SSR and API availability; allow optional intensityGuard with import.meta.client and optional chaining to avoid SSR/runtime surprises. Keeping default 'light' preserves current callers.
-function _useFeedback() { - function vibrate() { - if (hapticFeedback.impactOccurred.isAvailable()) { - hapticFeedback.impactOccurred('light') - } - } +function _useFeedback() { + function vibrate(intensity: 'light' | 'medium' | 'heavy' = 'light') { + if (import.meta.client && hapticFeedback?.impactOccurred?.isAvailable?.()) { + hapticFeedback.impactOccurred(intensity) + } + }apps/atrium-telegram/app/pages/index.vue (1)
43-43: UI spacing change acknowledgementmb-32 → mb-20 reduces bottom spacing; just ensure it doesn’t cause overlap with any fixed/absolute elements on short viewports.
apps/atrium-telegram/app/components/TaskCard.vue (1)
56-66: Minor: consider haptics on focus/unfocus actions for parityOptional: call vibrate() inside onFocus/onUnfocus before mutating state to keep feedback consistent across all actions.
Also applies to: 67-70
📜 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.
📒 Files selected for processing (10)
apps/atrium-telegram/app/assets/css/styles.css(1 hunks)apps/atrium-telegram/app/components/Button.vue(1 hunks)apps/atrium-telegram/app/components/CreateCard.vue(1 hunks)apps/atrium-telegram/app/components/Navigation.vue(2 hunks)apps/atrium-telegram/app/components/TaskCard.vue(4 hunks)apps/atrium-telegram/app/components/TaskList.vue(3 hunks)apps/atrium-telegram/app/components/TasksTodaySwitch.vue(1 hunks)apps/atrium-telegram/app/composables/useFeedback.ts(1 hunks)apps/atrium-telegram/app/pages/index.vue(4 hunks)apps/atrium-telegram/i18n/locales/ru-RU.json(2 hunks)
⏰ 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 (10)
apps/atrium-telegram/i18n/locales/ru-RU.json (2)
10-13: New keys look consistent.
Copy reads well and matches existing naming (task vs task-list).
24-33: No English locale present—no parity action needed
Theapps/atrium-telegram/i18n/localesdirectory only containsru-RU.json; there is no corresponding English JSON to sync against. This service relies on inline/default English strings in code, so adding these keys to another locale file isn’t applicable. If you introduce additional locale files in the future, you’ll want to mirror these keys there.apps/atrium-telegram/app/components/TasksTodaySwitch.vue (1)
17-17: LGTM on centralized feedback.
Using useFeedback().vibrate keeps haptics consistent.apps/atrium-telegram/app/components/Navigation.vue (1)
35-35: LGTM: migration to useFeedback.
Centralizing haptics removes repetitive availability checks.apps/atrium-telegram/app/components/Button.vue (1)
11-11: LGTM on using useFeedback.
Simpler and consistent with the PR-wide pattern.apps/atrium-telegram/app/components/CreateCard.vue (1)
2-2: Border width change: confirm it aligns with design tokens across cardsSwitching from border-2 to border may visually diverge from TaskList and other cards. If uniformity is desired, verify with design.
apps/atrium-telegram/app/pages/index.vue (2)
80-83: LGTM: avatar click now provides haptic feedback before opening the modalThe sequence (vibrate → open) is clear and aligned with the new composable.
51-55: Haptics on CreateCard when clicking outside the button depend on the componentAfter applying the CreateCard change to emit a click with vibrate from the root, this parent handler will receive the emitted click and users will get consistent haptics across the whole card. Please retest this interaction afterward.
apps/atrium-telegram/app/components/TaskCard.vue (1)
196-206: LGTM: tactile feedback before opening completion modalThe ordering (vibrate → guard → open → reset) reads well and avoids sticky checkbox state.
apps/atrium-telegram/app/components/TaskList.vue (1)
102-103: LGTM: centralized haptics via useFeedback and clean handlersHandlers are concise and consistent with the PR’s pattern.
Also applies to: 130-138
| @@ -1,16 +1,22 @@ | |||
| <template> | |||
| <div class="flex flex-col gap-4 justify-center items-center h-full min-h-40 border-2 border-default border-dashed rounded-lg"> | |||
| <div class="flex flex-col gap-4 justify-center items-center h-full min-h-40 border border-default border-dashed rounded-lg"> | |||
There was a problem hiding this comment.
🛠️ Refactor suggestion
Emit click from the component and vibrate on the root to ensure consistent haptics across the whole card
Right now, clicking the card background opens the modal (parent @click) but doesn’t vibrate; only the inner button vibrates. Centralize the behavior in CreateCard: vibrate and emit a click from the root, and remove the button-level handler.
Apply within these ranges:
- <div class="flex flex-col gap-4 justify-center items-center h-full min-h-40 border border-default border-dashed rounded-lg">
+ <div class="flex flex-col gap-4 justify-center items-center h-full min-h-40 border border-default border-dashed rounded-lg" @click="onClick"> :label="label"
- :ui="{
- label: 'font-medium',
- }"
- @click="vibrate"
+ :ui="buttonUi"Add outside the selected ranges in <script setup>:
const emit = defineEmits<{ (e: 'click', ev?: MouseEvent): void }>()
const buttonUi = { label: 'font-medium' }
function onClick(ev?: MouseEvent) {
vibrate()
emit('click', ev)
}Also applies to: 10-14
| <div v-if="canEdit" class="flex flex-row gap-2"> | ||
| <UTooltip :text="`Редактировать проект «${list?.name}»`"> | ||
| <UButton | ||
| variant="outline" | ||
| color="neutral" | ||
| size="md" | ||
| icon="i-lucide-pencil" | ||
| @click="modalUpdateTaskList.open({ listId })" | ||
| /> | ||
| </UTooltip> | ||
| <UButton | ||
| variant="outline" | ||
| color="neutral" | ||
| size="md" | ||
| icon="i-lucide-pencil" | ||
| @click="handleEditTaskList" | ||
| /> | ||
|
|
||
| <UTooltip :text="`${$t('app.create.task.button')} в проекте «${list?.name}»`"> | ||
| <UButton | ||
| variant="solid" | ||
| color="secondary" | ||
| size="md" | ||
| icon="i-lucide-plus" | ||
| @click="modalCreateTask.open({ performerId: userStore.id, listId })" | ||
| /> | ||
| </UTooltip> | ||
| <UButton | ||
| variant="solid" | ||
| color="secondary" | ||
| size="md" | ||
| icon="i-lucide-plus" | ||
| @click="handleCreateTask" | ||
| /> | ||
| </div> |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Icon-only buttons need accessible labels
Since tooltips were removed, add aria-label (and optionally title) so screen readers and hover users know the action.
<UButton
variant="outline"
color="neutral"
size="md"
icon="i-lucide-pencil"
+ aria-label="Редактировать список"
+ title="Редактировать список"
@click="handleEditTaskList"
/>
<UButton
variant="solid"
color="secondary"
size="md"
icon="i-lucide-plus"
+ aria-label="Создать задачу"
+ title="Создать задачу"
@click="handleCreateTask"
/>📝 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.
| <div v-if="canEdit" class="flex flex-row gap-2"> | |
| <UTooltip :text="`Редактировать проект «${list?.name}»`"> | |
| <UButton | |
| variant="outline" | |
| color="neutral" | |
| size="md" | |
| icon="i-lucide-pencil" | |
| @click="modalUpdateTaskList.open({ listId })" | |
| /> | |
| </UTooltip> | |
| <UButton | |
| variant="outline" | |
| color="neutral" | |
| size="md" | |
| icon="i-lucide-pencil" | |
| @click="handleEditTaskList" | |
| /> | |
| <UTooltip :text="`${$t('app.create.task.button')} в проекте «${list?.name}»`"> | |
| <UButton | |
| variant="solid" | |
| color="secondary" | |
| size="md" | |
| icon="i-lucide-plus" | |
| @click="modalCreateTask.open({ performerId: userStore.id, listId })" | |
| /> | |
| </UTooltip> | |
| <UButton | |
| variant="solid" | |
| color="secondary" | |
| size="md" | |
| icon="i-lucide-plus" | |
| @click="handleCreateTask" | |
| /> | |
| </div> | |
| <div v-if="canEdit" class="flex flex-row gap-2"> | |
| <UButton | |
| variant="outline" | |
| color="neutral" | |
| size="md" | |
| icon="i-lucide-pencil" | |
| aria-label="Редактировать список" | |
| title="Редактировать список" | |
| @click="handleEditTaskList" | |
| /> | |
| <UButton | |
| variant="solid" | |
| color="secondary" | |
| size="md" | |
| icon="i-lucide-plus" | |
| aria-label="Создать задачу" | |
| title="Создать задачу" | |
| @click="handleCreateTask" | |
| /> | |
| </div> |
🤖 Prompt for AI Agents
In apps/atrium-telegram/app/components/TaskList.vue around lines 56 to 72, the
icon-only UButton components lack accessible labels; add an aria-label for each
icon-only button (e.g., aria-label="Edit task list" on the pencil button and
aria-label="Create task" on the plus button) and optionally add a matching title
attribute for hover tooltips so screen readers and sighted users can understand
the action.
| } | ||
| } | ||
|
|
||
| export const useFeedback = createSharedComposable(_useFeedback) |
There was a problem hiding this comment.
Missing import: createSharedComposable will be undefined
This will throw at runtime. Import it from @vueuse/core.
+import { createSharedComposable } from '@vueuse/core'
import { hapticFeedback } from '@telegram-apps/sdk-vue'📝 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.
| export const useFeedback = createSharedComposable(_useFeedback) | |
| import { createSharedComposable } from '@vueuse/core' | |
| import { hapticFeedback } from '@telegram-apps/sdk-vue' | |
| export const useFeedback = createSharedComposable(_useFeedback) |
🤖 Prompt for AI Agents
In apps/atrium-telegram/app/composables/useFeedback.ts around line 15,
createSharedComposable is used but not imported which will cause a runtime
error; add the import statement "import { createSharedComposable } from
'@vueuse/core'" at the top of the file (with other imports) so
createSharedComposable is defined before exporting useFeedback.



Summary by CodeRabbit
New Features
Style
Refactor