chore: new photos on webinar#200
Conversation
WalkthroughAdds and reorganizes webinar UI: introduces several new components (ForWhom, WillKnow, Register, Reviews, InfiniteTitlesDividerReversed, ReviewCard), revises existing components (Countdown, Header, Hero, InfiniteTitlesDivider, SpeakerCard, Speakers), and restructures pages/index.vue to use the new components and updated section order. Multiple components add visibility-driven entrance animations. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant Page as pages/index.vue
participant Sections as ForWhom / WillKnow / Register / Countdown / ReviewCard
participant Vis as useElementVisibility
participant UI as Motion Classes
User->>Page: Load webinar page
Page->>Sections: Render components
loop On viewport scroll/resize
Sections->>Vis: Observe target ref visibility
Vis-->>Sections: targetIsVisible = true/false
alt targetIsVisible becomes true
Sections->>UI: set isVisible = true
UI-->>User: Apply entrance animation
else not visible yet
Sections->>UI: isVisible remains false
end
end
sequenceDiagram
autonumber
participant Divider as InfiniteTitlesDivider
participant Reversed as InfiniteTitlesDividerReversed
participant VM as onMounted
participant Carousel as UCarousel
VM->>Divider: Duplicate items once
VM->>Reversed: Duplicate items x5
Divider->>Carousel: Render items, no loop
Reversed->>Carousel: Start at last item, negative auto-scroll
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~30 minutes Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches🧪 Generate unit tests (beta)
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. Comment |
|
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (4)
apps/webinar/app/components/InfiniteTitlesDividerReversed.vue (1)
37-42: Items duplication logic is duplicated.This is the same items duplication pattern flagged in
InfiniteTitlesDivider.vue. Please refer to the refactoring suggestion in that file's review.apps/webinar/app/components/ReviewCard.vue (1)
34-44: Visibility animation pattern is duplicated.This is the same visibility animation pattern flagged in
Register.vue(lines 54-64). Please refer to the composable extraction suggestion in that file's review.apps/webinar/app/components/WillKnow.vue (1)
59-70: Duplicate visibility logic.This visibility-tracking pattern is identical to the one in Countdown.vue (lines 48-60). See the review comment on Countdown.vue for a refactoring suggestion to extract this into a reusable composable.
apps/webinar/app/components/ForWhom.vue (1)
47-58: Duplicate visibility logic.This visibility-tracking pattern is identical to the one in Countdown.vue (lines 48-60) and WillKnow.vue (lines 59-69). See the review comment on Countdown.vue for a refactoring suggestion to extract this into a reusable composable.
🧹 Nitpick comments (4)
apps/webinar/app/components/InfiniteTitlesDivider.vue (2)
38-42: Extract the items duplication logic into a shared utility.This items duplication pattern is identical in both
InfiniteTitlesDivider.vueandInfiniteTitlesDividerReversed.vue. Additionally, the magic number5lacks explanation.Create a utility function:
// utils/carousel.ts export function duplicateCarouselItems<T>(items: T[], times: number = 5): T[] { const result = [...items] for (let i = 0; i < times; i++) { result.push(...items) } return result }Then simplify the component:
onMounted(() => { - // add items - for (let i = 0; i < 5; i++) { - items.value.push(...items.value) - } + items.value = duplicateCarouselItems(items.value) })Consider documenting why 5 duplications are needed (e.g., for smooth infinite scrolling).
20-24: Remove commented code.The commented
<img>block should be removed rather than left in the codebase. If there's a need to preserve it for reference, consider using version control history instead.Apply this diff:
- <!-- <img - src="/sushi-heart.svg" - alt="" - class="w-10 h-auto opacity-90" - > -->apps/webinar/app/components/Hero.vue (1)
22-26: Consider extracting the webinar date and time to a configuration.The date (30 октября 2025) and time (12:00-13:30) are hardcoded here and also appear in
Register.vue(lines 26, 29). If the webinar date changes, you'll need to update multiple locations.Create a webinar configuration:
// config/webinar.ts export const WEBINAR_CONFIG = { date: '30 октября 2025', time: '12:00-13:30 (МСК)', registrationUrl: 'https://t.me/sign_up_for_webinars_bot', } as constThen use it in components:
<template> <h3 class="flex flex-row gap-2 items-center text-xl font-bold"> <UIcon name="i-lucide-calendar" class="size-8 text-secondary" /> {{ WEBINAR_CONFIG.date }} </h3> <h3 class="flex flex-row gap-2 items-center text-xl font-bold"> <UIcon name="i-lucide-clock" class="size-8 text-secondary" /> {{ WEBINAR_CONFIG.time }} </h3> </template> <script setup lang="ts"> import { WEBINAR_CONFIG } from '~/config/webinar' </script>apps/webinar/app/components/Reviews.vue (1)
7-11: Consider using a unique ID or index for the key.Using
review.user.nameas the key (line 9) could cause collisions if two reviews share the same name. Consider adding a uniqueidfield to each review object or use the array index as a fallback.- <ReviewCard - v-for="review in reviews" - :key="review.user.name" - :review="review" - /> + <ReviewCard + v-for="(review, index) in reviews" + :key="review.id ?? index" + :review="review" + />Then add an
idfield to each review object:const reviews = ref([{ + id: 1, user: { name: 'Сергей',
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (7)
apps/webinar/public/bg-1.jpgis excluded by!**/*.jpgapps/webinar/public/bg-2.jpgis excluded by!**/*.jpgapps/webinar/public/bg-countdown.pngis excluded by!**/*.pngapps/webinar/public/bg-know.pngis excluded by!**/*.pngapps/webinar/public/bg-register.pngis excluded by!**/*.pngapps/webinar/public/bg-whom.pngis excluded by!**/*.pngapps/webinar/public/sushi-heart-256.pngis excluded by!**/*.png
📒 Files selected for processing (13)
apps/webinar/app/components/Countdown.vue(1 hunks)apps/webinar/app/components/ForWhom.vue(1 hunks)apps/webinar/app/components/Header.vue(1 hunks)apps/webinar/app/components/Hero.vue(1 hunks)apps/webinar/app/components/InfiniteTitlesDivider.vue(2 hunks)apps/webinar/app/components/InfiniteTitlesDividerReversed.vue(1 hunks)apps/webinar/app/components/Register.vue(1 hunks)apps/webinar/app/components/ReviewCard.vue(1 hunks)apps/webinar/app/components/Reviews.vue(1 hunks)apps/webinar/app/components/SpeakerCard.vue(1 hunks)apps/webinar/app/components/Speakers.vue(1 hunks)apps/webinar/app/components/WillKnow.vue(1 hunks)apps/webinar/app/pages/index.vue(1 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 (5)
apps/webinar/app/components/Header.vue (1)
3-10: LGTM!The UNavigationMenu configuration with ui prop for item spacing and link styling is properly implemented.
apps/webinar/app/components/SpeakerCard.vue (1)
11-11: LGTM!The centering of speaker details is a straightforward presentational improvement that aligns with the overall UI updates.
apps/webinar/app/components/Speakers.vue (1)
4-9: LGTM!The updated title and description provide clearer context about the speakers, and the UI mapping correctly applies the new description styling.
apps/webinar/app/components/Register.vue (1)
34-35: Verify external link accessibility.
The registration button links to an external Telegram bot. Ensure that:
- Users are informed they’re leaving the site
- Links opened in a new tab (
target="_blank") includerel="noopener noreferrer"Confirm whether
UButtonappliesrel="noopener noreferrer"by default; if not, add it explicitly.apps/webinar/app/pages/index.vue (1)
1-21: LGTM!The page restructuring integrates the new components cleanly. The content flow is logical and the component composition is appropriate.
| <script setup lang="ts"> | ||
| const target = useTemplateRef<HTMLDivElement>('target') | ||
| const targetIsVisible = useElementVisibility(target) | ||
|
|
||
| const isVisible = ref(false) | ||
| watch(targetIsVisible, () => { | ||
| if (!targetIsVisible.value) { | ||
| return | ||
| } | ||
|
|
||
| isVisible.value = targetIsVisible.value | ||
| }) | ||
| </script> |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Extract duplicated visibility logic into a composable.
This exact visibility-tracking pattern (useTemplateRef → useElementVisibility → watch → isVisible flag) is duplicated across multiple components (Countdown.vue, ForWhom.vue lines 47-57, WillKnow.vue lines 59-69). This violates DRY and increases maintenance overhead.
Consider creating a composable:
// composables/useVisibilityAnimation.ts
export function useVisibilityAnimation(refName: string = 'target') {
const target = useTemplateRef<HTMLDivElement>(refName)
const targetIsVisible = useElementVisibility(target)
const isVisible = ref(false)
watch(targetIsVisible, () => {
if (!targetIsVisible.value) {
return
}
isVisible.value = targetIsVisible.value
})
return { target, isVisible }
}Then simplify each component's script to:
-const target = useTemplateRef<HTMLDivElement>('target')
-const targetIsVisible = useElementVisibility(target)
-
-const isVisible = ref(false)
-watch(targetIsVisible, () => {
- if (!targetIsVisible.value) {
- return
- }
-
- isVisible.value = targetIsVisible.value
-})
+const { target, isVisible } = useVisibilityAnimation()| const target = useTemplateRef<HTMLDivElement>('target') | ||
| const targetIsVisible = useElementVisibility(target) | ||
|
|
||
| const isVisible = ref(false) | ||
| watch(targetIsVisible, () => { | ||
| if (!targetIsVisible.value) { | ||
| return | ||
| } | ||
|
|
||
| isVisible.value = targetIsVisible.value | ||
| }) |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Extract the visibility animation pattern into a composable.
This visibility animation pattern is duplicated across multiple components (Register.vue, ReviewCard.vue, and likely others mentioned in the AI summary). The same logic using useTemplateRef, useElementVisibility, and a watcher to set isVisible appears in at least three places.
Create a composable to eliminate this duplication:
// composables/useVisibilityAnimation.ts
export function useVisibilityAnimation() {
const target = useTemplateRef<HTMLDivElement>('target')
const targetIsVisible = useElementVisibility(target)
const isVisible = ref(false)
watch(targetIsVisible, () => {
if (!targetIsVisible.value) {
return
}
isVisible.value = targetIsVisible.value
})
return { target, isVisible }
}Then simplify the component script:
-const target = useTemplateRef<HTMLDivElement>('target')
-const targetIsVisible = useElementVisibility(target)
-
-const isVisible = ref(false)
-watch(targetIsVisible, () => {
- if (!targetIsVisible.value) {
- return
- }
-
- isVisible.value = targetIsVisible.value
-})
+const { target, isVisible } = useVisibilityAnimation()| const reviews = ref([{ | ||
| user: { | ||
| name: 'Сергей', | ||
| description: 'Одинцово', | ||
| avatar: { | ||
| src: 'https://avatar.nextorders.ru/637024?emotion=8&gender=male&clothing=teal', | ||
| alt: '', | ||
| }, | ||
| }, | ||
| quote: 'Открыл суши-бар в августе 2025 года. На открытие ушел 1 месяц. В первый день выручка составила 70 тыс. руб. Считаю это неплохим показателем, учитывая, что один из поваров попал в аварию и мы вынуждены были справляться сами. Иначе выручки были бы еще больше.', | ||
| }, { | ||
| user: { | ||
| name: 'Надежда', | ||
| description: 'Зеленоградск', | ||
| avatar: { | ||
| src: 'https://avatar.nextorders.ru/219743?emotion=7&gender=female&clothing=violet', | ||
| alt: '', | ||
| }, | ||
| }, | ||
| quote: 'На открытие своего кафе в курортном городе у меня ушло 2 месяца. Мое кафе работает уже пару лет и приносит мне 3 млн.руб выручки в год. Треть из них моя чистая прибыль.', | ||
| }, { | ||
| user: { | ||
| name: 'Екатерина', | ||
| description: 'Гурьевск', | ||
| avatar: { | ||
| src: 'https://avatar.nextorders.ru/108633?emotion=9&gender=female&clothing=green', | ||
| alt: '', | ||
| }, | ||
| }, | ||
| quote: 'Я открыла свое заведение более 10 лет назад, которое работает и по сей день, принося, стабильный доход. Все бизнес процессы отточены, а управлению я уделяю не более 2-х часов в неделю.', | ||
| }]) |
There was a problem hiding this comment.
Add meaningful alt text for user avatars.
Lines 23, 33, and 43 have empty alt attributes. User avatars convey identity and should include descriptive alt text, such as alt: 'Фото Сергея' or similar.
Apply this diff:
user: {
name: 'Сергей',
description: 'Одинцово',
avatar: {
src: 'https://avatar.nextorders.ru/637024?emotion=8&gender=male&clothing=teal',
- alt: '',
+ alt: 'Фото Сергея',
},
},Repeat for the other two reviews with appropriate names.
📝 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.
| const reviews = ref([{ | |
| user: { | |
| name: 'Сергей', | |
| description: 'Одинцово', | |
| avatar: { | |
| src: 'https://avatar.nextorders.ru/637024?emotion=8&gender=male&clothing=teal', | |
| alt: '', | |
| }, | |
| }, | |
| quote: 'Открыл суши-бар в августе 2025 года. На открытие ушел 1 месяц. В первый день выручка составила 70 тыс. руб. Считаю это неплохим показателем, учитывая, что один из поваров попал в аварию и мы вынуждены были справляться сами. Иначе выручки были бы еще больше.', | |
| }, { | |
| user: { | |
| name: 'Надежда', | |
| description: 'Зеленоградск', | |
| avatar: { | |
| src: 'https://avatar.nextorders.ru/219743?emotion=7&gender=female&clothing=violet', | |
| alt: '', | |
| }, | |
| }, | |
| quote: 'На открытие своего кафе в курортном городе у меня ушло 2 месяца. Мое кафе работает уже пару лет и приносит мне 3 млн.руб выручки в год. Треть из них моя чистая прибыль.', | |
| }, { | |
| user: { | |
| name: 'Екатерина', | |
| description: 'Гурьевск', | |
| avatar: { | |
| src: 'https://avatar.nextorders.ru/108633?emotion=9&gender=female&clothing=green', | |
| alt: '', | |
| }, | |
| }, | |
| quote: 'Я открыла свое заведение более 10 лет назад, которое работает и по сей день, принося, стабильный доход. Все бизнес процессы отточены, а управлению я уделяю не более 2-х часов в неделю.', | |
| }]) | |
| const reviews = ref([{ | |
| user: { | |
| name: 'Сергей', | |
| description: 'Одинцово', | |
| avatar: { | |
| src: 'https://avatar.nextorders.ru/637024?emotion=8&gender=male&clothing=teal', | |
| alt: 'Фото Сергея', | |
| }, | |
| }, | |
| quote: 'Открыл суши-бар в августе 2025 года. На открытие ушел 1 месяц. В первый день выручка составила 70 тыс. руб. Считаю это неплохим показателем, учитывая, что один из поваров попал в аварию и мы вынуждены были справляться сами. Иначе выручки были бы еще больше.', | |
| }, { | |
| user: { | |
| name: 'Надежда', | |
| description: 'Зеленоградск', | |
| avatar: { | |
| src: 'https://avatar.nextorders.ru/219743?emotion=7&gender=female&clothing=violet', | |
| alt: 'Фото Надежды', | |
| }, | |
| }, | |
| quote: 'На открытие своего кафе в курортном городе у меня ушло 2 месяца. Мое кафе работает уже пару лет и приносит мне 3 млн.руб выручки в год. Треть из них моя чистая прибыль.', | |
| }, { | |
| user: { | |
| name: 'Екатерина', | |
| description: 'Гурьевск', | |
| avatar: { | |
| src: 'https://avatar.nextorders.ru/108633?emotion=9&gender=female&clothing=green', | |
| alt: 'Фото Екатерины', | |
| }, | |
| }, | |
| quote: 'Я открыла свое заведение более 10 лет назад, которое работает и по сей день, принося, стабильный доход. Все бизнес процессы отточены, а управлению я уделяю не более 2-х часов в неделю.', | |
| }]) |
🤖 Prompt for AI Agents
In apps/webinar/app/components/Reviews.vue around lines 17 to 47 the avatar
objects have empty alt attributes which should be meaningful for accessibility;
replace the empty alt strings with descriptive text for each user (e.g., for
Сергей set avatar.alt to "Фото Сергея", for Надежда set avatar.alt to "Фото
Надежды", and for Екатерина set avatar.alt to "Фото Екатерины") so each review's
avatar includes an appropriate alt value.



Summary by CodeRabbit
New Features
Style