Skip to content

Consider: Opportunities to re-use code/components #307

@mhsnook

Description

@mhsnook

Opportunities for Component Reuse

This document identifies areas where the Sunlo codebase has duplicated patterns that could be consolidated into shared components.

High Priority

1. Search Interfaces

Problem: Five distinct search interfaces exist with significant overlap in functionality.

Interface Location Search Type Results UI
Smart Search src/routes/_user/learn/$lang.search.tsx Server-side fuzzy (RPC) Accordion
Browse Page src/routes/_user/learn/browse.tsx Client-side includes Mixed cards
Chat Send Phrase src/routes/_user/friends/chats.$friendUid.recommend.tsx Client-side Custom render
Select Phrases src/components/comments/select-phrases-for-comment.tsx Client-side Checkboxes
Friend Search src/routes/_user/friends/search.tsx Client-side Profile cards

Shared Features:

  • Search input with debouncing (100-300ms)
  • Optional language/tag filtering
  • Results display with loading/empty states
  • useLanguagePhrasesSearch() hook used by 3 interfaces

Recommendation: Create a <PhraseSearchWidget> component accepting:

  • searchType: 'smart' | 'local' | 'picker'
  • renderResult: custom renderer function
  • onSelect: callback for selections
  • filters: optional { tags, languages }
  • sortable: boolean
  • maxSelections: number (for picker mode)

Files to consolidate:

  • src/routes/_user/friends/chats.$friendUid.recommend.tsx:40-127
  • src/components/comments/select-phrases-for-comment.tsx:36-195
  • Common display logic from src/routes/_user/learn/$lang.search.tsx

2. Deletion Dialogs

Problem: Three nearly identical deletion dialogs with 60+ lines of duplication each.

Dialog Location Target
Delete Comment src/components/comments/delete-comment-dialog.tsx Comments
Delete Playlist src/components/playlists/delete-playlist-dialog.tsx Playlists
Delete Request src/components/requests/delete-request-dialog.tsx Requests

Identical Pattern:

- AlertDialog wrapper
- useState for open state
- useMutation for deletion
- Collection update on success
- Toast notification
- Optional navigation away

Recommendation: Create <DeleteConfirmDialog>:

interface DeleteDialogProps {
  trigger: ReactNode
  title: string
  description: string
  mutationKey: string[]
  deleteFn: () => Promise<void>
  onSuccess?: () => void
  navigateAway?: string
}

Files to consolidate:

  • src/components/comments/delete-comment-dialog.tsx
  • src/components/playlists/delete-playlist-dialog.tsx
  • src/components/requests/delete-request-dialog.tsx

3. Upvote Buttons

Problem: Two nearly identical upvote button implementations.

Button Location
Upvote Request src/components/requests/upvote-request-button.tsx:17-97
Upvote Playlist src/components/playlists/upvote-playlist-button.tsx:17-97

Identical Logic:

  • Check existing upvote via live query
  • Toggle mutation (insert/delete)
  • Heart icon with filled/outline state
  • Count display with animation

Recommendation: Create generic <UpvoteButton>:

interface UpvoteButtonProps {
  entityType: 'request' | 'playlist'
  entityId: uuid
  lang: string
  count: number
}

4. Edit/Update Dialogs

Problem: Two identical single-textarea update dialogs.

Dialog Location
Update Request src/components/requests/update-request-dialog.tsx
Update Comment src/components/comments/update-comment-dialog.tsx

Identical Pattern:

  • Dialog with textarea
  • Cancel resets state
  • Save mutation updates collection
  • Toast notification

Recommendation: Create <EditTextDialog>:

interface EditTextDialogProps {
  trigger: ReactNode
  title: string
  value: string
  placeholder?: string
  onSave: (value: string) => Promise<void>
}

Medium Priority

5. Phrase Display Variants

Problem: Multiple components display phrase data with different layouts.

Component Location Context
CardResultSimple src/components/cards/card-result-simple.tsx Comments, contributions
PhraseTinyCard src/components/cards/phrase-tiny-card.tsx Grids, recommendations
PhraseAccordionItem src/components/phrase-accordion-item.tsx Accordion lists
PhraseSummaryLine src/components/feed/feed-phrase-group-item.tsx:10-77 Feed items

Shared Data:

  • Phrase text
  • Translations
  • Language badge
  • Heart/status indicator
  • Learner count

Recommendation: Create <PhraseDisplay> with variant prop:

interface PhraseDisplayProps {
  phrase: PhraseFullType
  variant: 'card' | 'tiny' | 'accordion' | 'summary' | 'inline'
  interactive?: boolean
  onSelect?: () => void
}

6. Send to Friend Dialogs

Problem: Two nearly identical "send to friend" dialogs.

Dialog Location
Send Playlist src/components/playlists/send-playlist-to-friend.tsx
Send Request src/components/card-pieces/send-request-to-friend.tsx

Identical Logic:

  • Dialog with SelectMultipleFriends component
  • Mutation sends chat message
  • Auth requirement wrapper

Recommendation: Create <SendToFriendsDialog>:

interface SendToFriendsDialogProps {
  trigger: ReactNode
  entityType: 'playlist' | 'request' | 'phrase'
  entityId: uuid
  lang: string
}

7. Empty State Components

Problem: Inconsistent empty state patterns across lists and feeds.

Current Patterns:

  1. Callout with CTA buttons (feed pages)
  2. Simple italic text (friend lists)
  3. Icon + heading + description (browse page)

Recommendation: Create <EmptyState>:

interface EmptyStateProps {
  icon?: LucideIcon
  title: string
  description?: string
  actions?: Array<{ label: string; to: string; variant?: ButtonVariant }>
}

Files using inconsistent patterns:

  • src/routes/_user/learn/$lang.feed.tsx:198-222 (Callout pattern)
  • src/components/friend-profiles.tsx:25 (Text pattern)
  • src/routes/_user/learn/browse.tsx:465-476 (Centered text pattern)

8. Infinite Scroll Trigger

Problem: Infinite scroll pattern duplicated with slight variations.

Current Implementations:

  • src/routes/_user/learn/$lang.search.tsx:245-249 (IntersectionObserver)
  • src/routes/_user/learn/$lang.feed.tsx:236-243 (Load More button)

Recommendation: Create <InfiniteScrollTrigger>:

interface InfiniteScrollTriggerProps {
  hasNextPage: boolean
  isFetching: boolean
  onLoadMore: () => void
  mode?: 'intersection' | 'button'
}

Lower Priority

9. Creator Header Pattern

Observation: Multiple components display creator info + timestamp + owner actions.

Appears in:

  • Request headers
  • Playlist headers
  • Comment headers
  • Feed item headers

Already somewhat abstracted: UidPermalink and UidPermalinkInline handle user display.

Potential improvement: Extract <ItemCreatorHeader> combining:

  • User permalink
  • Timestamp (via ago())
  • Conditional owner action buttons

10. Form Mutation Pattern

Observation: Most forms follow identical mutation success/error handling.

Pattern:

onSuccess: (data) => {
  collection.utils.writeInsert/writeUpdate(Schema.parse(data))
  toast.success('Message')
}
onError: (error) => {
  console.log('Error', error)
  toast.error('Error message')
}

Potential improvement: Create useFormMutation hook:

function useFormMutation<TData, TInput>({
  mutationFn,
  collection,
  schema,
  successMessage,
  errorMessage,
  onSuccess,
}: FormMutationOptions<TData, TInput>)

11. Unused Item Component

Observation: src/components/ui/item.tsx defines a comprehensive Item component system that is NOT currently used anywhere in the codebase.

Components available:

  • Item, ItemGroup, ItemSeparator
  • ItemMedia, ItemContent, ItemTitle, ItemDescription
  • ItemActions, ItemHeader, ItemFooter

Opportunity: Consider using this for standardizing list item layouts instead of custom implementations.


Implementation Notes

Prioritization Criteria

  1. Code duplication - Highest priority for nearly identical implementations
  2. Maintenance burden - Components that need synchronized updates
  3. Consistency - User-facing patterns that should look/behave identically

Suggested Implementation Order

  1. Delete dialogs (quick win, high duplication)
  2. Upvote buttons (quick win, high duplication)
  3. Edit text dialogs (quick win)
  4. Search widget (larger effort, high impact)
  5. Phrase display variants (larger effort, medium impact)
  6. Send to friends dialogs
  7. Empty states
  8. Infinite scroll trigger

Testing Considerations

  • All consolidated components should maintain existing behavior
  • Consider creating shared test utilities for common patterns
  • Playwright tests should not break if visual layout remains similar

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    Status

    📋 Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions