feat: new blocks#183
Conversation
WalkthroughAdds new webinar UI components (Hero, Header, Countdown, CountdownTimer, CountdownTimerItem, HeroVideo), updates page assembly to use them, adjusts styles for HeaderLogo and InfiniteTitlesDivider, changes nuxt routeRules to prerender all routes, and removes the sharp dependency. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant Browser
participant NuxtApp as Nuxt App (Client)
participant Components as Header/Hero/Countdown
participant Timer as CountdownTimer (Interval)
User->>Browser: Navigate to /
Browser->>NuxtApp: Load app & hydrate
NuxtApp->>Components: Render Header and Hero
NuxtApp->>Components: Render Countdown
Components->>Timer: mount()
Timer->>Timer: setInterval(1000)
loop Every 1s
Timer->>Timer: compute D/H/M/S to 2025-10-25T00:00:00
Timer-->>Components: update reactive state
Components-->>Browser: re-render timer values
end
note over Timer: clearInterval on unmount
sequenceDiagram
autonumber
participant Dev as Build
participant Nuxt as Nuxt Prerender
participant Static as Static Output
participant CDN as Server/CDN
participant User as Browser
Dev->>Nuxt: Build with routeRules '/**': { prerender: true }
Nuxt->>Static: Generate static HTML/Assets
Static->>CDN: Deploy files
User->>CDN: Request page
CDN-->>User: Serve prerendered HTML
note over User: Client hydrates Vue components
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 inconclusive)
✅ Passed checks (2 passed)
✨ 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. Comment |
|
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/webinar/package.json (1)
15-23: Sharp removal is not safe – still used directly and transitively
- apps/web-app/server/api/**/image.post.ts import and use sharp for image processing
- apps/core-telegram, apps/web-storefront and apps/geo-vault package.json still list sharp
- pnpm-workspace.yaml lists sharp under onlyBuiltDependencies
- packages/ui/nuxt.config.ts includes '@nuxt/image' (which pulls in sharp)
Retain or replace all these usages before removing sharp.
🧹 Nitpick comments (4)
apps/webinar/app/components/InfiniteTitlesDivider.vue (1)
28-40: Avoid self-referential push causing exponential growth and potential jank.Mutating the same array while reading from it is harder to reason about and scales 32x items. Use concat to avoid referencing the same array during mutation (keeps behavior but safer).
onMounted(() => { // add items for (let i = 0; i < 5; i++) { - items.value.push(...items.value) + // Avoid reading and writing from the same array reference in one op + items.value = items.value.concat(items.value) } })Optionally, compute repeats based on viewport width to avoid unnecessary DOM nodes.
apps/webinar/nuxt.config.ts (1)
9-10: Prefer scoping prerender to pages; avoid applying to API/globally.Prerendering all routes can affect server routes and build time. Scope to pages and explicitly exclude APIs.
- '/**': { prerender: true }, + '/': { prerender: true }, + '/api/**': { prerender: false },If more pages exist, list them or use nitro.prerender.routes. Verify no auth-related routes are unintentionally prerendered with
nuxt-auth-utils.apps/webinar/app/components/HeroVideo.vue (1)
2-8: Reduce bandwidth and improve mobile behavior for video.Add preload="metadata" and playsinline. Consider moving the URL to runtime config for easier updates.
<video - controls + controls + preload="metadata" + playsinline class="max-h-180 w-auto mx-auto rounded-xl" >Example using runtime config (outside this diff):
<script setup lang="ts"> const { public: { heroVideoUrl } } = useRuntimeConfig() </script> <template> <video controls preload="metadata" playsinline class="max-h-180 w-auto mx-auto rounded-xl"> <source :src="heroVideoUrl" type="video/mp4"> Ваш браузер не поддерживает видео </video> </template>apps/webinar/app/components/CountdownTimerItem.vue (1)
1-14: LGTM: simple, typed presentational unit.No issues spotted. If you need zero-padding (e.g., 02), consider formatting upstream or adding an optional pad prop later.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (2)
apps/webinar/public/sushi-main-logo.pngis excluded by!**/*.pngpnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (11)
apps/webinar/app/components/Countdown.vue(1 hunks)apps/webinar/app/components/CountdownTimer.vue(1 hunks)apps/webinar/app/components/CountdownTimerItem.vue(1 hunks)apps/webinar/app/components/Header.vue(1 hunks)apps/webinar/app/components/HeaderLogo.vue(1 hunks)apps/webinar/app/components/Hero.vue(1 hunks)apps/webinar/app/components/HeroVideo.vue(1 hunks)apps/webinar/app/components/InfiniteTitlesDivider.vue(1 hunks)apps/webinar/app/pages/index.vue(2 hunks)apps/webinar/nuxt.config.ts(1 hunks)apps/webinar/package.json(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 (3)
apps/webinar/app/components/InfiniteTitlesDivider.vue (1)
14-22: LGTM: visual adjustments improve contrast.Text color and icon opacity changes look good on the yellow background.
apps/webinar/app/pages/index.vue (1)
2-4: Verify Countdown is SSR-safe under prerender.If Countdown uses timers/window on setup, wrap with to avoid hydration issues with global prerendering.
If needed:
<ClientOnly> <Countdown /> </ClientOnly>Also applies to: 25-29
apps/webinar/app/components/Countdown.vue (1)
1-32: LGTM!The countdown block is well composed and aligns with the surrounding UI. No issues spotted.
| const interval = setInterval(() => { | ||
| const now = new Date() | ||
| const diff = target.value.getTime() - now.getTime() | ||
| state.value = { | ||
| days: Math.floor(diff / 86400000), | ||
| hours: Math.floor((diff % 86400000) / 3600000), | ||
| minutes: Math.floor((diff % 3600000) / 60000), | ||
| seconds: Math.floor((diff % 60000) / 1000), | ||
| } |
There was a problem hiding this comment.
Prevent negative countdown values after the target date.
Once the webinar date passes, diff becomes negative and the UI starts showing negative days/hours/minutes, which is a poor user experience. Clamp the countdown at zero and stop the interval when the date is reached; also compute the first tick immediately so the timer doesn’t sit at all zeros for a full second after mount.
Apply this diff:
+const SECOND = 1000
+const MINUTE = SECOND * 60
+const HOUR = MINUTE * 60
+const DAY = HOUR * 24
+
+const state = ref({
+ days: 0,
+ hours: 0,
+ minutes: 0,
+ seconds: 0,
+})
+
+let interval: ReturnType<typeof setInterval>
+
+const updateCountdown = () => {
+ const diff = target.value.getTime() - Date.now()
+
+ if (diff <= 0) {
+ state.value = { days: 0, hours: 0, minutes: 0, seconds: 0 }
+ clearInterval(interval)
+ return
+ }
+
+ state.value = {
+ days: Math.floor(diff / DAY),
+ hours: Math.floor((diff % DAY) / HOUR),
+ minutes: Math.floor((diff % HOUR) / MINUTE),
+ seconds: Math.floor((diff % MINUTE) / SECOND),
+ }
+}
+
onMounted(() => {
- const interval = setInterval(() => {
- const now = new Date()
- const diff = target.value.getTime() - now.getTime()
- state.value = {
- days: Math.floor(diff / 86400000),
- hours: Math.floor((diff % 86400000) / 3600000),
- minutes: Math.floor((diff % 3600000) / 60000),
- seconds: Math.floor((diff % 60000) / 1000),
- }
- }, 1000)
-
- onBeforeUnmount(() => clearInterval(interval))
+ updateCountdown()
+ interval = setInterval(updateCountdown, 1000)
})
+
+onBeforeUnmount(() => clearInterval(interval))📝 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 interval = setInterval(() => { | |
| const now = new Date() | |
| const diff = target.value.getTime() - now.getTime() | |
| state.value = { | |
| days: Math.floor(diff / 86400000), | |
| hours: Math.floor((diff % 86400000) / 3600000), | |
| minutes: Math.floor((diff % 3600000) / 60000), | |
| seconds: Math.floor((diff % 60000) / 1000), | |
| } | |
| const SECOND = 1000 | |
| const MINUTE = SECOND * 60 | |
| const HOUR = MINUTE * 60 | |
| const DAY = HOUR * 24 | |
| const state = ref({ | |
| days: 0, | |
| hours: 0, | |
| minutes: 0, | |
| seconds: 0, | |
| }) | |
| let interval: ReturnType<typeof setInterval> | |
| const updateCountdown = () => { | |
| const diff = target.value.getTime() - Date.now() | |
| if (diff <= 0) { | |
| state.value = { days: 0, hours: 0, minutes: 0, seconds: 0 } | |
| clearInterval(interval) | |
| return | |
| } | |
| state.value = { | |
| days: Math.floor(diff / DAY), | |
| hours: Math.floor((diff % DAY) / HOUR), | |
| minutes: Math.floor((diff % HOUR) / MINUTE), | |
| seconds: Math.floor((diff % MINUTE) / SECOND), | |
| } | |
| } | |
| onMounted(() => { | |
| updateCountdown() | |
| interval = setInterval(updateCountdown, 1000) | |
| }) | |
| onBeforeUnmount(() => clearInterval(interval)) |
🤖 Prompt for AI Agents
In apps/webinar/app/components/CountdownTimer.vue around lines 21 to 29, the
interval handler lets diff go negative causing negative time values and waits
one second before first tick; fix by computing the tick immediately after
mounting, clamping diff to a minimum of 0 before converting to
days/hours/minutes/seconds, and when diff <= 0 clearInterval(interval) to stop
the timer; update state using the clamped values and ensure the interval
reference is cleared to avoid memory leaks.
| <UColorModeButton size="xl" /> | ||
| <UButton | ||
| to="https://t.me/SLFranchiseBot" | ||
| size="xl" | ||
| color="neutral" | ||
| variant="ghost" | ||
| icon="simple-icons:telegram" | ||
| /> | ||
| <UButton | ||
| to="https://vk.com/franshizasushi" | ||
| size="xl" | ||
| color="neutral" | ||
| variant="ghost" | ||
| icon="simple-icons:vk" | ||
| /> | ||
| </div> | ||
| </div> |
There was a problem hiding this comment.
Icon-only external buttons need accessible labels and safe external targets.
Add aria-labels and open external links safely in new tabs.
<UButton
to="https://t.me/SLFranchiseBot"
+ target="_blank"
+ rel="noopener noreferrer"
+ aria-label="Открыть Telegram"
size="xl"
color="neutral"
variant="ghost"
icon="simple-icons:telegram"
/>
<UButton
to="https://vk.com/franshizasushi"
+ target="_blank"
+ rel="noopener noreferrer"
+ aria-label="Открыть VK"
size="xl"
color="neutral"
variant="ghost"
icon="simple-icons:vk"
/>📝 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.
| <UColorModeButton size="xl" /> | |
| <UButton | |
| to="https://t.me/SLFranchiseBot" | |
| size="xl" | |
| color="neutral" | |
| variant="ghost" | |
| icon="simple-icons:telegram" | |
| /> | |
| <UButton | |
| to="https://vk.com/franshizasushi" | |
| size="xl" | |
| color="neutral" | |
| variant="ghost" | |
| icon="simple-icons:vk" | |
| /> | |
| </div> | |
| </div> | |
| <UColorModeButton size="xl" /> | |
| <UButton | |
| to="https://t.me/SLFranchiseBot" | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| aria-label="Открыть Telegram" | |
| size="xl" | |
| color="neutral" | |
| variant="ghost" | |
| icon="simple-icons:telegram" | |
| /> | |
| <UButton | |
| to="https://vk.com/franshizasushi" | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| aria-label="Открыть VK" | |
| size="xl" | |
| color="neutral" | |
| variant="ghost" | |
| icon="simple-icons:vk" | |
| /> | |
| </div> | |
| </div> |
🤖 Prompt for AI Agents
In apps/webinar/app/components/Header.vue around lines 7 to 23, the icon-only
UButton components lack accessible labels and do not open external links safely;
add aria-label attributes (e.g., aria-label="Telegram" and aria-label="VK") to
each icon-only UButton so screen readers can identify them, and ensure external
links open in a new tab with safe attributes by adding target="_blank" and
rel="noopener noreferrer" (or the equivalent UButton props) to those buttons.
| src="/sushi-main-logo.png" | ||
| alt="" | ||
| class="mx-auto h-20 w-auto motion-preset-pop" | ||
| class="h-12 w-fit motion-preset-pop" | ||
| > |
There was a problem hiding this comment.
Add meaningful alt text for accessibility.
The logo is content, not purely decorative; empty alt hides it from SR.
<img
src="/sushi-main-logo.png"
- alt=""
+ alt="Sushi Love"
class="h-12 w-fit motion-preset-pop"
>📝 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.
| src="/sushi-main-logo.png" | |
| alt="" | |
| class="mx-auto h-20 w-auto motion-preset-pop" | |
| class="h-12 w-fit motion-preset-pop" | |
| > | |
| <img | |
| src="/sushi-main-logo.png" | |
| alt="Sushi Love" | |
| class="h-12 w-fit motion-preset-pop" | |
| > |
🤖 Prompt for AI Agents
In apps/webinar/app/components/HeaderLogo.vue around lines 3 to 6, the img tag
uses an empty alt attribute which hides meaningful logo content from screen
readers; replace the empty alt with a concise, descriptive string (for example
the application or company name like "Sushi Webinar logo" or whatever the brand
is) to convey the logo's purpose; keep it short, avoid redundant phrases like
"image of", and update any tests or snapshots if they assert on alt text.
| label: 'Записаться', | ||
| to: 'https://t.me/SLFranchiseBot', | ||
| target: '_blank', | ||
| trailingIcon: 'i-lucide-arrow-right', | ||
| ui: { | ||
| base: 'px-6 text-xl', | ||
| }, | ||
| }, |
There was a problem hiding this comment.
Add rel="noopener noreferrer" to the external link.
With target="_blank", omitting rel="noopener noreferrer" leaves the opener window exposed to reverse-tabnabbing. Please include the rel attribute to close that hole.
Apply this diff:
target: '_blank',
+ rel: 'noopener noreferrer',📝 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.
| label: 'Записаться', | |
| to: 'https://t.me/SLFranchiseBot', | |
| target: '_blank', | |
| trailingIcon: 'i-lucide-arrow-right', | |
| ui: { | |
| base: 'px-6 text-xl', | |
| }, | |
| }, | |
| label: 'Записаться', | |
| to: 'https://t.me/SLFranchiseBot', | |
| target: '_blank', | |
| rel: 'noopener noreferrer', | |
| trailingIcon: 'i-lucide-arrow-right', | |
| ui: { | |
| base: 'px-6 text-xl', | |
| }, | |
| }, |
🤖 Prompt for AI Agents
In apps/webinar/app/components/Hero.vue around lines 20 to 27, the button/link
object sets target: '_blank' but lacks rel attributes; add rel: 'noopener
noreferrer' to that object (e.g., alongside to and target) so external links
opened in a new tab do not expose the opener (prevent reverse-tabnabbing).



Summary by CodeRabbit