-
-
Notifications
You must be signed in to change notification settings - Fork 683
feat: add service expiry badge, service tag & category to addons #612
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
- Implemented ServiceExpiryBadge component for displaying service expiry status with customizable colors and text. - Developed ServiceExpiryDatePicker component for selecting expiry dates, including navigation between months and displaying the current date. - Introduced useServiceExpiry hook to manage service expiry logic, including fetching expiry data from various providers and caching results. - Added utility functions for handling service expiry preferences, caching, and calculating days remaining until expiry.
…r additional services
…ngs and use Zod for ISO format
WalkthroughThis PR introduces comprehensive service expiry management functionality. It extends the schema to support Changes
Sequence DiagramsequenceDiagram
actor User
participant Menu as Services/Addons<br/>Component
participant Modal as Service/Addon<br/>Modal
participant Hook as useServiceExpiry<br/>Hook
participant Cache as localStorage<br/>& Cache
participant API as Debrid<br/>Provider API
participant UI as Expiry Badge<br/>& Date Picker
User->>Menu: Open service/addon menu
Menu->>Hook: useServiceExpiry(serviceId)
Hook->>Cache: Check cached expiry
alt Cached & Valid
Cache-->>Hook: Return cached expiry
else Cache Expired/Missing
Hook->>Hook: Check expiry preference
alt Mode: Auto-Fetch
Hook->>API: Fetch expiry with credentials
API-->>Hook: Return expiryDate
Hook->>Cache: Store in cache (6h TTL)
else Mode: Manual
Hook->>Cache: Read stored manual date
Cache-->>Hook: Return manual date
else Mode: Hidden
Hook-->>Hook: Skip expiry fetch
end
end
Hook-->>Menu: ExpiryStatus (badge colours, text, tooltip)
Menu->>UI: Render ServiceExpiryBadge
UI-->>User: Display expiry indicator
User->>Modal: Configure service/addon settings
Modal->>Modal: Initialize with userTag/userCategory
User->>Modal: Select tag type or change date
Modal->>Modal: Validate required fields
User->>Modal: Submit changes
Modal->>Hook: Update expiry preference
Hook->>Cache: Persist preference
Modal-->>User: Confirm & Close
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~60 minutes This change spans multiple architectural layers with heterogeneous edits: schema extensions, validation logic, provider-specific API integration, localStorage-based state management, new UI components with internal date logic, metadata configuration, and integration into existing feature menus. Each area requires distinct reasoning despite forming a coherent feature. Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🤖 Fix all issues with AI Agents
In @packages/frontend/src/components/menu/service-expiry-badge.tsx:
- Around line 1-29: The menu-specific component ServiceExpiryBadge conflicts
with the UI badge component; rename the menu variant to a distinct identifier
(e.g., ServiceExpiryTextBadge) by changing the exported const and the memo'ed
function name from ServiceExpiryBadge to ServiceExpiryTextBadge and preserving
its props and behavior, then update all imports/usages (e.g., in
menu/services.tsx) to import ServiceExpiryTextBadge instead of
ServiceExpiryBadge so the two components no longer clash.
In @packages/frontend/src/components/menu/services.tsx:
- Around line 66-75: The Content component currently returns early when status
is falsy before all hooks run, violating Rules of Hooks; move the early return
so all hooks (useStatus, useUserData, useMenu, useState declarations, useEffect
hooks at lines referenced, and useSensors) are invoked unconditionally at the
top of Content, then inside the bodies of the affected useEffect callbacks and
any other logic (e.g., the useSensors-related code) add guarded checks like `if
(!status) return;` or conditional branches to avoid running effect logic when
status is falsy; ensure state hooks (modalOpen, modalService, modalValues,
isDragging) remain declared before the early return and only UI rendering is
skipped by the relocated return.
🧹 Nitpick comments (9)
packages/frontend/src/hooks/useServiceExpiry.ts (3)
54-57:JSON.stringifyfor credential signature may have inconsistent ordering.Object property ordering in
JSON.stringifyis generally consistent in modern JS engines, but relying on it for memoisation dependencies can be fragile if credentials are reconstructed with different property orders elsewhere.Consider using a more deterministic approach:
🔎 Suggested improvement
const credentialSignature = useMemo( - () => JSON.stringify(credentials ?? {}), + () => { + if (!credentials) return ''; + return Object.keys(credentials).sort().map(k => `${k}:${credentials[k] ?? ''}`).join('|'); + }, [credentials] );
234-244: Consider adding request timeout to external API calls.The
fetchcalls to external debrid provider APIs have no timeout configured. If a provider is slow or unresponsive, this could cause the hook to remain in a loading state indefinitely.🔎 Suggested implementation using AbortController
+const FETCH_TIMEOUT_MS = 10000; + async function fetchRealDebridExpiry( credentials: Credentials ): Promise<string | null> { const apiKey = credentials?.apiKey?.trim(); if (!apiKey) { throw new Error('Enter your Real-Debrid API key to view expiry.'); } + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); const response = await fetch('https://api.real-debrid.com/rest/1.0/user', { headers: { Authorization: `Bearer ${apiKey}`, }, cache: 'no-store', + signal: controller.signal, - }); + }).finally(() => clearTimeout(timeoutId)); if (!response.ok) { throw new Error(`Real-Debrid API error (${response.status})`); }This pattern should be applied to all provider fetch functions.
279-284: Timestamp magnitude heuristic is reasonable but add a clarifying comment.The logic to detect whether a timestamp is in seconds or milliseconds based on magnitude is practical, but a comment would help future maintainers understand the rationale.
🔎 Suggested comment
const secondsRaw = Number(user?.premiumUntil ?? user?.premium_until ?? 0); if (Number.isFinite(secondsRaw) && secondsRaw > 0) { + // Heuristic: values > 1 trillion are likely milliseconds (post ~2001), + // otherwise assume Unix seconds const millis = secondsRaw > 1_000_000_000_000 ? secondsRaw : secondsRaw * 1000; return new Date(millis).toISOString(); }packages/frontend/src/components/menu/service-expiry-date-picker.tsx (1)
117-133: Consider adding accessibility attributes to date buttons.The date buttons lack
aria-labelattributes, which would improve screen reader support.🔎 Suggested improvement
return ( <button key={`${weekIndex}-${dayIndex}`} type="button" + aria-label={date.toLocaleDateString(undefined, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })} + aria-selected={isSelected} className={cn( 'h-9 w-9 rounded-md transition text-sm flex items-center justify-center', isSelected ? 'bg-[--brand] text-white font-semibold shadow' : 'bg-[--paper] hover:bg-[--subtle] border border-transparent', isToday && !isSelected ? 'border border-[--brand] text-[--brand]' : null )} onClick={() => handleSelect(date)} > {date.getDate()} </button> );packages/frontend/src/components/menu/services.tsx (1)
631-637: Redundant state update before modal close.
setManualExpiryUpdatedAt(trimmed ? Date.now() : null)on line 636 is set immediately beforeonSubmitcloses the modal. The updated state is never observed as the modal resets on close.🔎 Proposed fix
} else { setExpiryPreference(serviceId, { mode: 'manual', date: trimmed ? trimmed : undefined, }); - setManualExpiryUpdatedAt(trimmed ? Date.now() : null); } } onSubmit(localValues);packages/frontend/src/components/menu/addons.tsx (2)
432-451: Mutable variable in render may cause issues with React's concurrent features.The
lastCategoryvariable is mutated during the map iteration. While this works in synchronous rendering, it can produce inconsistent results if React re-renders parts of the list during concurrent mode.🔎 Proposed fix: pre-compute groups
- (() => { - // Group addons by user category and render with headers - let lastCategory: UserAddonCategory | undefined = - undefined; - return userData.presets.map((preset) => { + (() => { + // Pre-compute which presets should show headers + const presetsWithHeaders = userData.presets.map((preset, idx) => { + const userCategory = preset.options?.userCategory as UserAddonCategory | undefined; + const prevCategory = idx > 0 + ? userData.presets[idx - 1]?.options?.userCategory + : undefined; + return { + preset, + showHeader: !!userCategory && userCategory !== prevCategory, + }; + }); + return presetsWithHeaders.map(({ preset, showHeader }) => { const presetMetadata = status?.settings?.presets.find( (p: any) => p.ID === preset.type ); - const userCategory = preset.options - ?.userCategory as UserAddonCategory | undefined; + const userCategory = preset.options?.userCategory as UserAddonCategory | undefined; const categoryMeta = getUserCategoryMetadata(userCategory); - const showHeader = - !!userCategory && userCategory !== lastCategory; const categoryLabel = categoryMeta?.label; const categoryColor = categoryMeta?.color; - if (userCategory) { - lastCategory = userCategory; - }
1262-1276: Consider addingopento dependency array.This effect sets default values for
userTaganduserCategory. It currently only depends oncurrentCategoryValue, but may benefit from re-running when the modal opens to ensure fresh defaults.useEffect(() => { setValues((prev) => { const nextOptions = { ...(prev.options ?? {}) }; let changed = false; if (!('userTag' in nextOptions)) { nextOptions.userTag = DEFAULT_USER_TAG; changed = true; } if (!('userCategory' in nextOptions) && currentCategoryValue) { nextOptions.userCategory = currentCategoryValue; changed = true; } return changed ? { ...prev, options: nextOptions } : prev; }); - }, [currentCategoryValue]); + }, [currentCategoryValue, open]);packages/frontend/src/components/ui/badge/badge.tsx (1)
121-133: Date parsing lacks validation for malformed input.
parseLocalDatesplits on'-'and converts to numbers without validating the result. IfexpiryDateis malformed (e.g.,"invalid"),Number("invalid")returnsNaN, creating an invalid Date.🔎 Proposed fix: add validation
// Parse date string as local date (not UTC) to avoid timezone issues const parseLocalDate = (dateStr: string) => { + if (!dateStr || !/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) { + return null; + } const [year, month, day] = dateStr.split('-').map(Number); + if ([year, month, day].some((n) => Number.isNaN(n))) { + return null; + } return new Date(year, month - 1, day); }; const isExpired = () => { if (normalizedType !== 'expires' || !expiryDate) return false; const expiry = parseLocalDate(expiryDate); + if (!expiry) return false; const today = new Date(); today.setHours(0, 0, 0, 0); expiry.setHours(0, 0, 0, 0); return expiry < today; };packages/frontend/src/utils/service-expiry.ts (1)
189-195: Redundant localStorage write when preference doesn't exist.When
!preference || preference.mode === 'auto'andoverrides[serviceId]doesn't exist,writeOverrides(overrides)is called unnecessarily on line 194.🔎 Proposed fix
if (!preference || preference.mode === 'auto') { if (overrides[serviceId]) { delete overrides[serviceId]; writeOverrides(overrides); - } else { - writeOverrides(overrides); } } else {
📜 Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (12)
packages/core/src/db/schemas.tspackages/core/src/utils/config.tspackages/frontend/src/components/menu/addons.tsxpackages/frontend/src/components/menu/service-expiry-badge.tsxpackages/frontend/src/components/menu/service-expiry-date-picker.tsxpackages/frontend/src/components/menu/services.tsxpackages/frontend/src/components/menu/user-addon-metadata.tspackages/frontend/src/components/shared/template-option.tsxpackages/frontend/src/components/ui/badge/badge.tsxpackages/frontend/src/components/ui/badge/index.tsxpackages/frontend/src/hooks/useServiceExpiry.tspackages/frontend/src/utils/service-expiry.ts
🧰 Additional context used
🧬 Code graph analysis (3)
packages/frontend/src/components/menu/addons.tsx (3)
packages/frontend/src/components/menu/user-addon-metadata.ts (6)
UserAddonCategory(41-41)getUserCategoryMetadata(66-74)UserTagType(85-85)getUserCategoryOptions(54-64)getUserTagOptions(95-104)DEFAULT_USER_TAG(111-111)packages/frontend/src/components/ui/badge/badge.tsx (1)
ServiceExpiryBadge(108-176)scripts/generateMetadata.cjs (1)
tag(11-11)
packages/frontend/src/components/shared/template-option.tsx (3)
packages/frontend/src/components/menu/service-expiry-date-picker.tsx (1)
ServiceExpiryDatePicker(17-158)packages/frontend/src/components/menu/user-addon-metadata.ts (3)
DEFAULT_USER_TAG(111-111)UserTagType(85-85)USER_TAG_OPTIONS(78-83)packages/frontend/src/components/ui/badge/badge.tsx (1)
ServiceExpiryBadge(108-176)
packages/frontend/src/utils/service-expiry.ts (1)
packages/core/src/utils/constants.ts (1)
ServiceId(243-243)
🪛 Biome (2.1.2)
packages/frontend/src/components/menu/services.tsx
[error] 136-136: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
Hooks should not be called after an early return.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
[error] 171-171: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
Hooks should not be called after an early return.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
[error] 172-172: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
Hooks should not be called after an early return.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
[error] 170-170: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
Hooks should not be called after an early return.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
[error] 180-180: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
Hooks should not be called after an early return.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
🔇 Additional comments (15)
packages/frontend/src/hooks/useServiceExpiry.ts (1)
177-205: LGTM - Return value construction is clean and well-structured.The success path correctly derives badge text, colours, and tooltip from the state. The conditional logic for handling different statuses (disabled, error, loading, idle) before the success path is clear.
packages/core/src/db/schemas.ts (1)
198-199: LGTM - New option types added correctly.The addition of
'date'and'service-tag'to theOptionDefinition.typeenum is clean and follows the existing pattern. The corresponding validation logic inpackages/core/src/utils/config.tsproperly handles these new types.packages/frontend/src/components/ui/badge/index.tsx (1)
1-2: LGTM - Clean barrel exports for badge module.Standard pattern for re-exporting components and types from a module index file.
packages/core/src/utils/config.ts (2)
929-942: LGTM - Date option validation is correct.The validation properly:
- Checks for non-empty string
- Uses
z.iso.date()for ISO date format validation (YYYY-MM-DD)- Provides clear error messages
944-972: LGTM - Service-tag option validation is thorough.The validation correctly handles the object structure with:
- Required
typefield (string)- Optional
expiryDatefield with ISO date validation when presentOne minor observation: the validation allows any string for
value.typewithout restricting to known tag types. This appears intentional to support custom tags.packages/frontend/src/components/menu/service-expiry-date-picker.tsx (1)
160-226: LGTM - Utility functions are well-implemented.The date utility functions are correct:
today()creates a date at midnight local timeparseDate()validates parsed components match the constructed date (catches invalid dates like Feb 30)formatDate()produces ISO format with zero-paddingbuildCalendar()correctly handles leading/trailing empty slots for week alignmentpackages/frontend/src/components/shared/template-option.tsx (3)
17-23: LGTM - Imports are correctly structured.The imports bring in the necessary components and metadata for the new option types. Note that
ServiceExpiryBadgehere is imported from../ui/badge, which is the component withtagType/expiryDateprops (distinct from the one inservice-expiry-badge.tsx).
414-433: LGTM - Date option case is well-implemented.The date option case correctly:
- Renders a labelled date picker
- Handles forced/value/default precedence
- Respects
emptyIsUndefinedbehaviour- Includes optional description rendering
434-520: LGTM - Service-tag case handles complex state well.The implementation correctly:
- Derives current value with proper fallback chain
- Dynamically adds custom tag types to options if not present
- Clears
expiryDatewhen switching away from 'expires' type- Shows conditional date picker and preview badge
One minor suggestion: the
ensureLabelfunction (lines 452-461) could be extracted as a utility if needed elsewhere.packages/frontend/src/components/menu/services.tsx (1)
505-537: LGTM!The
useServiceExpiryhook integration and conditional badge rendering are well implemented. The expiry status handling covers success, error, and disabled states appropriately.packages/frontend/src/components/menu/addons.tsx (1)
923-934: LGTM!The badge integration is clean with appropriate responsive hiding on smaller screens. The tag type extraction with fallback to
'none'ensures safe rendering.packages/frontend/src/components/ui/badge/badge.tsx (1)
59-96: LGTM!The Badge component properly forwards refs, handles custom colour overrides, and integrates well with the CVA variant system. The intent switching to
'custom'whenbackgroundColoris provided is a clean pattern.packages/frontend/src/components/menu/user-addon-metadata.ts (1)
1-111: LGTM!Well-structured metadata module with:
- Consistent category and tag option definitions
- Proper fallback handling for unknown values via
formatLabel- Derived metadata objects for efficient lookups
- Clear separation between options and metadata utilities
packages/frontend/src/utils/service-expiry.ts (2)
219-241: LGTM!The date normalisation logic correctly:
- Appends
T23:59:59to date-only strings so expiry occurs at end of day- Handles space-separated datetime formats as fallback
- Returns
nullfor unparseable datesThe
calculateDaysRemainingcorrectly usesMath.ceilto round up partial days.
1-51: Well-structured expiry management module.Good practices observed:
- SSR-safe guards throughout (
typeof window === 'undefined')- Clear separation between cache and preference storage
- Custom event (
EXPIRY_OVERRIDE_EVENT) for reactive updates- Explicit service ID lists for feature gating
| import { memo } from 'react'; | ||
|
|
||
| interface ServiceExpiryBadgeProps { | ||
| text: string; | ||
| colors: { | ||
| background: string; | ||
| foreground: string; | ||
| }; | ||
| title?: string; | ||
| } | ||
|
|
||
| export const ServiceExpiryBadge = memo(function ServiceExpiryBadge({ | ||
| text, | ||
| colors, | ||
| title, | ||
| }: ServiceExpiryBadgeProps) { | ||
| return ( | ||
| <span | ||
| className="ml-2 inline-flex shrink-0 items-center rounded px-2 py-0.5 text-[0.65rem] font-semibold uppercase leading-none shadow-sm" | ||
| style={{ | ||
| backgroundColor: colors.background, | ||
| color: colors.foreground, | ||
| }} | ||
| title={title} | ||
| > | ||
| {text} | ||
| </span> | ||
| ); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check for ServiceExpiryBadge definitions and usages
echo "=== ServiceExpiryBadge exports ==="
rg -n "export.*ServiceExpiryBadge" --type=tsx --type=ts
echo ""
echo "=== All ServiceExpiryBadge references ==="
rg -n "ServiceExpiryBadge" --type=tsx --type=tsRepository: Viren070/AIOStreams
Length of output: 213
🏁 Script executed:
#!/bin/bash
# Check for ServiceExpiryBadge definitions and usages
echo "=== ServiceExpiryBadge exports ==="
rg -n "export.*ServiceExpiryBadge" -t ts
echo ""
echo "=== All ServiceExpiryBadge references ==="
rg -n "ServiceExpiryBadge" -t ts
echo ""
echo "=== Check for badge.tsx file ==="
fd "badge.tsx" packages/frontend
echo ""
echo "=== Service expiry badge related files ==="
fd "service-expiry" packages/frontendRepository: Viren070/AIOStreams
Length of output: 2790
🏁 Script executed:
sed -n '99,115p' packages/frontend/src/components/ui/badge/badge.tsxRepository: Viren070/AIOStreams
Length of output: 617
Two ServiceExpiryBadge components exist with conflicting names and different interfaces.
There are two components exported with the same name:
packages/frontend/src/components/menu/service-expiry-badge.tsx: acceptstextandcolorspropspackages/frontend/src/components/ui/badge/badge.tsx: acceptstagType,expiryDate, andclassNameprops
Each is used in different contexts (menu/services.tsx uses the menu version; template-option.tsx and addons.tsx use the ui/badge version), but the identical names create confusion and increase the risk of importing the wrong component.
Rename the menu version to something more specific (e.g., ServiceExpiryTextBadge) to clearly distinguish it from the ui badge component.
🤖 Prompt for AI Agents
In @packages/frontend/src/components/menu/service-expiry-badge.tsx around lines
1 - 29, The menu-specific component ServiceExpiryBadge conflicts with the UI
badge component; rename the menu variant to a distinct identifier (e.g.,
ServiceExpiryTextBadge) by changing the exported const and the memo'ed function
name from ServiceExpiryBadge to ServiceExpiryTextBadge and preserving its props
and behavior, then update all imports/usages (e.g., in menu/services.tsx) to
import ServiceExpiryTextBadge instead of ServiceExpiryBadge so the two
components no longer clash.
| function Content() { | ||
| const { status } = useStatus(); | ||
| const { setUserData, userData } = useUserData(); | ||
| const { setSelectedMenu, nextMenu, previousMenu } = useMenu(); | ||
| const [modalOpen, setModalOpen] = useState(false); | ||
| const [modalService, setModalService] = useState<ServiceId | null>(null); | ||
| const [modalValues, setModalValues] = useState<Record<string, any>>({}); | ||
| const [isDragging, setIsDragging] = useState(false); | ||
|
|
||
| if (!status) return null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hooks called conditionally after early return violates React's Rules of Hooks.
The early return on line 75 causes useEffect (lines 136, 180) and useSensors (line 170) to be called conditionally. React requires hooks to be called in the same order on every render.
🔎 Proposed fix: move early return after all hooks
function Content() {
const { status } = useStatus();
const { setUserData, userData } = useUserData();
const { setSelectedMenu, nextMenu, previousMenu } = useMenu();
const [modalOpen, setModalOpen] = useState(false);
const [modalService, setModalService] = useState<ServiceId | null>(null);
const [modalValues, setModalValues] = useState<Record<string, any>>({});
const [isDragging, setIsDragging] = useState(false);
- if (!status) return null;
+ const sensors = useSensors(
+ useSensor(PointerSensor),
+ useSensor(TouchSensor, {
+ activationConstraint: {
+ delay: 150,
+ tolerance: 8,
+ },
+ })
+ );
+
+ useEffect(() => {
+ if (!status) return;
+ // ... existing effect logic
+ }, [status, status?.settings.services, userData.services]);
+
+ useEffect(() => {
+ // ... existing isDragging effect logic
+ }, [isDragging]);
+
+ if (!status) return null;Move all hook declarations before the early return, and guard the effect bodies with conditionals instead.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In @packages/frontend/src/components/menu/services.tsx around lines 66 - 75, The
Content component currently returns early when status is falsy before all hooks
run, violating Rules of Hooks; move the early return so all hooks (useStatus,
useUserData, useMenu, useState declarations, useEffect hooks at lines
referenced, and useSensors) are invoked unconditionally at the top of Content,
then inside the bodies of the affected useEffect callbacks and any other logic
(e.g., the useSensors-related code) add guarded checks like `if (!status)
return;` or conditional branches to avoid running effect logic when status is
falsy; ensure state hooks (modalOpen, modalService, modalValues, isDragging)
remain declared before the early return and only UI rendering is skipped by the
relocated return.
Summary by CodeRabbit
Release Notes
New Features
✏️ Tip: You can customize this high-level summary in your review settings.