Skip to content

Conversation

@goodgpa
Copy link

@goodgpa goodgpa commented Jan 6, 2026

Summary by CodeRabbit

Release Notes

New Features

  • Added addon categorisation with visual grouping and category-based organisation
  • Introduced service expiry tracking with visual status badges
  • Added support for new date and service-tag option types with automatic validation
  • Implemented date picker interface for managing expiry dates
  • Enhanced addon display with expiry status indicators and colour-coded categories

✏️ Tip: You can customize this high-level summary in your review settings.

- 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.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 6, 2026

Walkthrough

This PR introduces comprehensive service expiry management functionality. It extends the schema to support date and service-tag option types, adds validation for these new types, implements expiry status tracking and preference management with localStorage caching, and integrates expiry-aware UI components into the services and addons menus. The feature includes category-based addon grouping, expiry date selection, badge rendering, and provider-specific expiry fetching logic across multiple debrid services.

Changes

Cohort / File(s) Summary
Schema & Validation
packages/core/src/db/schemas.ts, packages/core/src/utils/config.ts
Extended OptionDefinition.type enum with 'date' and 'service-tag' literals. Added validation branches in validateOption() for date (ISO format via z.iso.date()) and service-tag (object with type field and optional ISO expiryDate).
Expiry Management Utilities
packages/frontend/src/utils/service-expiry.ts
New module providing localStorage-backed caching (6-hour TTL), expiry preference storage (auto/manual/hidden modes), tracked service definitions, badge color derivation, and expiry override event emission. Exports types (ServiceExpirySource, ExpiryMode, ExpiryPreference, CachedExpiryEntry), constants (TRACKED_SERVICE_IDS, AUTO_FETCH_SERVICE_IDS), and utilities (getCachedExpiry, getExpiryPreference, calculateDaysRemaining, getBadgeColors, formatExpiryTitle).
Expiry State Hook
packages/frontend/src/hooks/useServiceExpiry.ts
New React hook managing expiry status computation with support for multiple debrid providers (realdebrid, alldebrid, premiumize, debridlink, torbox). Handles auto-fetch, manual, and hidden modes with credential-based API fetching, caching, state management (idle/loading/success/error), and badge colour/tooltip generation.
UI Components: Badges
packages/frontend/src/components/ui/badge/badge.tsx, packages/frontend/src/components/ui/badge/index.tsx
New styled Badge component with CVA-based variants. New ServiceExpiryBadge that renders expiry status (lifetime/free/expires/custom) with computed intent (purple/success/danger/warning/info) based on tag type and expiry date. Index file re-exports both components and types.
UI Components: Expiry
packages/frontend/src/components/menu/service-expiry-badge.tsx, packages/frontend/src/components/menu/service-expiry-date-picker.tsx
New memoized ServiceExpiryBadge component accepting text and colour props with optional tooltip. New ServiceExpiryDatePicker component with popover-based calendar UI, month navigation, date selection (YYYY-MM-DD), and Today/Clear actions. Includes internal utility functions for date parsing, formatting, and calendar grid generation.
Addon Metadata & Configuration
packages/frontend/src/components/menu/user-addon-metadata.ts
New module centralizing user addon category and tag definitions. Exports category and tag options with labels and colours, type aliases (UserAddonCategory, UserTagType), metadata mappings, and utility functions (getUserCategoryOptions, getUserCategoryMetadata, getUserTagOptions, getUserTagLabel) with support for unknown/current values. Defines DEFAULT_USER_TAG.
Template Option Rendering
packages/frontend/src/components/shared/template-option.tsx
Extended switch statement to handle new 'date' case (renders ServiceExpiryDatePicker with value mapping and onClear) and 'service-tag' case (renders Select for tag type, conditional date picker when type is 'expires', badge preview, with fallback to USER_TAG_OPTIONS and DEFAULT_USER_TAG).
Feature Integration: Addons Menu
packages/frontend/src/components/menu/addons.tsx
Enhanced Installed addons view with category-based grouping (dynamic headers with category labels/colours). SortableAddonItem now displays left border colour reflecting category and renders ServiceExpiryBadge when userTag.type is present. AddonModal adds normalization logic, auto-injects userCategory and userTag fields with defaults, derives category/tag options from metadata, and validates all required fields before submission.
Feature Integration: Services Menu
packages/frontend/src/components/menu/services.tsx
Refactored with expiry-aware logic via useServiceExpiry() hook. SortableServiceItem and Content now render expiry badges and date pickers conditionally. ServiceModal adds expiry mode selection (auto/manual/hidden), manual expiry input field, preference persistence via getExpiryPreference/setExpiryPreference, and applies expiry settings on submit alongside credential changes. Updated imports for expiry utilities and UI components.

Sequence Diagram

sequenceDiagram
    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
Loading

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

🐰 ✨ Hops through the code with expiry in sight,
Badges and dates, now gleaming so bright,
Categories grouped with colours so fine,
Debrid services tracked, the perfect design!
From schema to UI, the feature takes flight! 🎉

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.67% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: add service expiry badge, service tag & category to addons' directly describes the main changes, which include new expiry badge functionality, service tag support, and category grouping for addons across multiple frontend components.
✨ Finishing touches
  • 📝 Generate docstrings

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

Comment @coderabbitai help to get the list of available commands and usage tips.

@goodgpa goodgpa marked this pull request as ready for review January 6, 2026 16:33
Copy link
Contributor

@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: 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.stringify for credential signature may have inconsistent ordering.

Object property ordering in JSON.stringify is 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 fetch calls 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-label attributes, 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 before onSubmit closes 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 lastCategory variable 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 adding open to dependency array.

This effect sets default values for userTag and userCategory. It currently only depends on currentCategoryValue, 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.

parseLocalDate splits on '-' and converts to numbers without validating the result. If expiryDate is malformed (e.g., "invalid"), Number("invalid") returns NaN, 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' and overrides[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

📥 Commits

Reviewing files that changed from the base of the PR and between 80a244b and 5a7633d.

📒 Files selected for processing (12)
  • packages/core/src/db/schemas.ts
  • packages/core/src/utils/config.ts
  • packages/frontend/src/components/menu/addons.tsx
  • packages/frontend/src/components/menu/service-expiry-badge.tsx
  • packages/frontend/src/components/menu/service-expiry-date-picker.tsx
  • packages/frontend/src/components/menu/services.tsx
  • packages/frontend/src/components/menu/user-addon-metadata.ts
  • packages/frontend/src/components/shared/template-option.tsx
  • packages/frontend/src/components/ui/badge/badge.tsx
  • packages/frontend/src/components/ui/badge/index.tsx
  • packages/frontend/src/hooks/useServiceExpiry.ts
  • packages/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 the OptionDefinition.type enum is clean and follows the existing pattern. The corresponding validation logic in packages/core/src/utils/config.ts properly 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:

  1. Checks for non-empty string
  2. Uses z.iso.date() for ISO date format validation (YYYY-MM-DD)
  3. Provides clear error messages

944-972: LGTM - Service-tag option validation is thorough.

The validation correctly handles the object structure with:

  • Required type field (string)
  • Optional expiryDate field with ISO date validation when present

One minor observation: the validation allows any string for value.type without 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 time
  • parseDate() validates parsed components match the constructed date (catches invalid dates like Feb 30)
  • formatDate() produces ISO format with zero-padding
  • buildCalendar() correctly handles leading/trailing empty slots for week alignment
packages/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 ServiceExpiryBadge here is imported from ../ui/badge, which is the component with tagType/expiryDate props (distinct from the one in service-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 emptyIsUndefined behaviour
  • 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 expiryDate when switching away from 'expires' type
  • Shows conditional date picker and preview badge

One minor suggestion: the ensureLabel function (lines 452-461) could be extracted as a utility if needed elsewhere.

packages/frontend/src/components/menu/services.tsx (1)

505-537: LGTM!

The useServiceExpiry hook 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' when backgroundColor is 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:59 to date-only strings so expiry occurs at end of day
  • Handles space-separated datetime formats as fallback
  • Returns null for unparseable dates

The calculateDaysRemaining correctly uses Math.ceil to 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

Comment on lines +1 to +29
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>
);
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 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=ts

Repository: 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/frontend

Repository: Viren070/AIOStreams

Length of output: 2790


🏁 Script executed:

sed -n '99,115p' packages/frontend/src/components/ui/badge/badge.tsx

Repository: 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: accepts text and colors props
  • packages/frontend/src/components/ui/badge/badge.tsx: accepts tagType, expiryDate, and className props

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.

Comment on lines +66 to +75
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;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

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.

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