Skip to content

Feature/Search/683 #805

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

Closed
wants to merge 86 commits into from
Closed

Feature/Search/683 #805

wants to merge 86 commits into from

Conversation

EthanFennell
Copy link
Contributor

@EthanFennell EthanFennell commented Apr 23, 2025

Implementation of the Search page in DAB including:

  • Navigation
  • Search form with options for matching whole words, accents, tones
  • Results view with ability to select words

Implementation details of the LexiconXMLView were revised to be re-used here in an efficient manner. Queries are returning results of many words quickly now.

Summary by CodeRabbit

  • New Features
    • Introduced new components: AlphabetStrip, LexiconReversalView, LexiconXMLView, and a lexicon search page for enhanced lexicon navigation and search.
    • Added dictionary search functionality supporting accent and tone options.
  • Enhancements
    • Improved styling and theming across lexicon list, entry, and navigation components using CSS variables for consistent appearance.
    • Updated navigation logic in the sidebar for program-specific search routing.
    • Enhanced word navigation and selection logic in lexicon views.
  • Bug Fixes
    • Improved handling of XML rendering and clickable references in lexicon entry views.
  • Chores
    • Updated sql.js dependency to version 1.13.0.
  • Refactor
    • Shifted word list management in WordNavigationStrip to use internal stores.
    • Streamlined prop usage and event handling in several components.
  • Documentation
    • Updated and clarified component prop interfaces.

Copy link
Contributor

coderabbitai bot commented Apr 23, 2025

Walkthrough

This update introduces several new Svelte components, including AlphabetStrip, LexiconReversalView, and LexiconXmlView, and implements a new lexicon search page at src/routes/lexicon/search/+page.svelte. The search logic is enhanced with a new searchDictionary function and an updated SearchOptions interface supporting accent and tone matching. UI components such as SearchForm, Sidebar, WordNavigationStrip, and various lexicon list and entry views receive styling improvements and internal logic adjustments, particularly around theme color variables and store-based state management. The package dependency for sql.js is also updated.

Changes

File(s) Change Summary
package.json Updated sql.js dependency version from ^1.12.0 to ^1.13.0.
src/lib/components/AlphabetStrip.svelte New component for rendering a horizontally scrollable strip of alphabet buttons, with props for alphabet array, active letter, and selection callback.
src/lib/components/LexiconReversalView.svelte New component combining AlphabetStrip and LexiconLanguageTabs for reversal lexicon view, managing current letter selection and language switching via props and callbacks.
src/lib/components/LexiconXMLView.svelte New component to render XML-formatted lexicon entries for given word IDs, with dynamic SQL querying, XML parsing, formatting, and interactive anchor replacement.
src/lib/components/SearchForm.svelte Conditional logic for special character buttons based on programType, updated styling using CSS variables, and refined rendering conditions for special input buttons and checkboxes.
src/lib/components/Sidebar.svelte Added goToSearch function to centralize navigation logic for search, with conditional routing based on programType.
src/lib/components/WordNavigationStrip.svelte Removed external wordsList prop; now derives word list from Svelte stores. Updated styling to use theme CSS variables.
src/lib/search-worker/dab-search-worker.ts Added new searchDictionary function for dictionary search with support for accent/tone and whole word matching, deduplication, and weighted sorting.
src/lib/search/domain/interfaces/data-interfaces.ts Extended SearchOptions interface with optional accentsAndTones boolean property.
src/routes/lexicon/+page.svelte Refactored grid layout, added showBackButton state, replaced LexiconEntryView with LexiconXmlView, and improved scroll container usage for word lists and details.
src/routes/lexicon/search/+page.svelte New search page with navbar, search form, results display, word navigation, and XML entry rendering. Manages search state, options, and navigation.
src/lib/components/LexiconReversalListView.svelte,
src/lib/components/LexiconVernacularListView.svelte
Updated list and button styling to use CSS variables for background, text, and border colors.
src/lib/components/LexiconEntryView.svelte Enhanced XML rendering: improved theming via CSS variables, styled anchor replacements, and robust event listener management for clickable spans.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant SearchForm
  participant dab-search-worker
  participant LexiconXmlView

  User->>SearchForm: Submit search phrase & options
  SearchForm->>dab-search-worker: searchDictionary(phrase, options)
  dab-search-worker-->>SearchForm: [wordIds]
  SearchForm->>LexiconXmlView: Pass wordIds
  LexiconXmlView->>LexiconXmlView: Query DB, parse XML, render entries
  User->>LexiconXmlView: Click anchor (word reference)
  LexiconXmlView->>SearchForm: onSelectWord callback
Loading

Possibly related PRs

  • Feature/display reversals/685 #802: Adds a new LexiconLanguageTabs component and discusses UI improvements for language selector tabs, directly relating to the introduction and use of LexiconReversalView and LexiconLanguageTabs in this update.

Poem

A strip of letters, bright and neat,
With tabs and views, our lexicon’s complete.
Search flows smoother, accents now in tow,
XML shines with a themed, gentle glow.
New words and colors, a rabbit’s delight—
Hop, search, and read, from morning to night!
🐇✨


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0b11d66 and 7f89651.

📒 Files selected for processing (4)
  • src/lib/components/LexiconEntryView.svelte (1 hunks)
  • src/lib/components/LexiconXMLView.svelte (1 hunks)
  • src/lib/components/WordNavigationStrip.svelte (3 hunks)
  • src/routes/lexicon/+page.svelte (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/lib/components/LexiconXMLView.svelte
  • src/lib/components/LexiconEntryView.svelte
  • src/lib/components/WordNavigationStrip.svelte
  • src/routes/lexicon/+page.svelte

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

aidanpscott and others added 23 commits April 23, 2025 15:35
Create component for reversal view

Create reusable alphabet strip component

Implement initial lexicon reversal interface

Add lexicon page data loading

Add DAB program type route handling

Add missing imports to lexicon and main routing pages

lint format for page.js file for lexicon

Create first reversal page draft

Converted page to components

Dynamically load alphabet from dictionary config

Dynamically load alphabet from dictionary config

Lift language state to parent component

Add reversal lanuage name and letter change detection

Lazy load English words and base for XML implementation

Create Language Tabs Component and Parse lang
Issue #685

Moved the code for the language tabs from
LexiconReversalView and created a separate
component. Also, parsed lang from writing systems
and updated index.d.ts with displayLang

Change English wording and fix selectedLetter

Name vernacular language correctly and organize code

Create new index.ts file for lexicon page

Name vernacular language correctly and organize code

Fix language tab naming convention

Removed duplicate code for parsing lang

Issue #685

Removed Unnecessary Code and Lexicon Folder

Issue #685

Removed hardcoding

Switch order of language tabs
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.

Caution

Inline review comments failed to post. This is likely due to GitHub's limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 17

♻️ Duplicate comments (1)
src/routes/lexicon/+page.svelte (1)

158-161: ⚠️ Potential issue

Same null‑selection bug as in Search page

Dereferencing selectedWord after potentially setting it to null will crash. Apply the same guard fix here.

🧹 Nitpick comments (17)
src/lib/components/AlphabetStrip.svelte (1)

7-9: Consider adding keyboard navigation to the alphabet strip.

For better accessibility, implement keyboard navigation that allows users to move through the letters using arrow keys.

<script lang="ts">
    export let alphabet: string[];
    export let activeLetter: string;
    export let onLetterSelect: (letter: string) => void;
+    
+    function handleKeydown(event: KeyboardEvent) {
+        if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return;
+        
+        const currentIndex = alphabet.indexOf(activeLetter);
+        let newIndex: number;
+        
+        if (event.key === 'ArrowLeft') {
+            newIndex = currentIndex > 0 ? currentIndex - 1 : alphabet.length - 1;
+        } else {
+            newIndex = currentIndex < alphabet.length - 1 ? currentIndex + 1 : 0;
+        }
+        
+        onLetterSelect(alphabet[newIndex]);
+    }
</script>

<div
    class="flex m-2 gap-1 md:gap-2 mb-4 justify-start overflow-x-auto whitespace-nowrap pb-2 snap-x min-w-[100vw]"
+    tabindex="0"
+    role="toolbar"
+    aria-label="Alphabet navigation"
+    on:keydown={handleKeydown}
>
src/routes/quiz/[collection]/[id]/+page.js (2)

5-8: Function signature and validation look good, but consider improving error message clarity

The validation logic correctly checks for the presence of writing systems configuration. However, the error message could be more specific to help with troubleshooting.

- throw new Error('Writing systems configuration not found');
+ throw new Error('Configuration error: config.writingSystems is missing or empty');

18-26: Consider adding more robust error handling for initial data loading

The code silently continues if there's an error loading the initial reversal data, which could lead to subtle issues later. Consider adding more detailed error handling or at least a fallback mechanism.

let initialReversalData = {};
try {
    const response = await fetch(`${base}/reversal/language/${defaultKey}/${alphabet[0]}.json`);
    if (response.ok) {
        initialReversalData = await response.json();
+   } else {
+       console.warn(`Failed to load initial reversal data: ${response.status} ${response.statusText}`);
    }
} catch (error) {
    console.error('Error loading initial reversal data:', error);
}
src/lib/search-worker/dab-search-worker.ts (3)

10-19: Consider optimizing database initialization and console logging

The database initialization might be expensive, and console logging large result sets could impact performance in production.

- let db = await initializeDatabase({ fetch });
+ // Reuse database connection if possible
+ const db = await initializeDatabase({ fetch });

const dynamicQuery = searchWords.map(() => `${column} LIKE ?`).join(' OR ');
const dynamicParams = searchWords.map((word) => (options.wholeWords ? word : `%${word}%`));
results = db.exec(`SELECT locations FROM search_words WHERE ${dynamicQuery}`, dynamicParams);
- console.log('results:', results);
+ // Use more targeted logging or remove in production
+ if (process.env.NODE_ENV !== 'production') {
+     console.log('Search results count:', results?.[0]?.values?.length || 0);
+ }

21-27: The flatMap and processing logic is efficient, but could benefit from comments

The location extraction logic is somewhat complex. Consider adding more detailed comments to explain the data format and processing steps.

// Extract and process locations from the query result
+ // Format of locations: "id1(weight1) id2(weight2) ..."
+ // We parse this into an array of objects with id and weight properties
let locations = results[0].values.flatMap((value) =>
    value[0].split(' ').map((loc) => {
        const [id, weight] = loc.split('(').map((v) => v.replace(')', ''));
        return { id: parseInt(id, 10), weight: parseInt(weight, 10) };
    })
);

41-44: Consider adding a limit to the number of results returned

For performance reasons, you might want to limit the number of results returned, especially for very common search terms.

locations = locations.sort((a, b) => b.weight - a.weight);
+ // Limit results to avoid overwhelming the UI
+ const MAX_RESULTS = 100; // Adjust as needed
+ locations = locations.slice(0, MAX_RESULTS);
const ids = locations.map((location) => location.id);
return ids;
src/lib/components/SearchForm.svelte (2)

13-15: Remove commented-out code

Commented-out code should be removed before merging to production to keep the codebase clean.

- // const specialCharacters =
- //     config.mainFeatures['input-buttons']?.split(' ').filter((c) => c.length) ?? [];
-

83-83: Consider refactoring repetitive conditional logic

The conditions for displaying UI elements follow the same pattern and could be refactored to reduce duplication.

You could create a helper function at the top of the script section:

<script lang="ts">
    // ...existing imports
    
    // Helper function to determine if a feature should be displayed
    function shouldShowFeature(featureName: string): boolean {
        return !!config.mainFeatures[featureName] || config.programType === 'DAB';
    }
    
    // ...rest of script
</script>

<!-- Then in the template: -->
{#if shouldShowFeature('search-input-buttons') && specialCharacters.length > 0}
    <!-- Special characters section -->
{/if}

{#if shouldShowFeature('search-whole-words-show')}
    <!-- Whole words section -->
{/if}

{#if shouldShowFeature('search-accents-show')}
    <!-- Accents section -->
{/if}

Also applies to: 98-98, 108-108

src/lib/components/LexiconLanguageTabs.svelte (1)

11-45: Extract hardcoded animation values to variables and add aria-label attributes

The animation values are hardcoded and the tabs could benefit from aria-label attributes for better accessibility.

<div class="flex w-full" style="background-color: var(--TabBackgroundColor);">
+   <!-- Animation constants could be defined in the script section -->
+   <!-- const ANIMATION_DISTANCE = 70; -->
    <div
        role="button"
        tabindex="0"
        aria-pressed={selectedLanguage === vernacularLanguage}
+       aria-label={`Switch to ${vernacularLanguage} language`}
        on:click={() => onSwitchLanguage(vernacularLanguage)}
        on:keydown={(e) => e.key === 'Enter' && onSwitchLanguage(vernacularLanguage)}
        class="py-2.5 px-3.5 text-sm uppercase text-center relative dy-tabs dy-tabs-bordered mb-1"
    >
        {vernacularLanguage}
        {#if selectedLanguage === vernacularLanguage}
            <div
-               transition:fly={{ axis: 'x', easing: expoInOut, x: 70 }}
+               transition:fly={{ axis: 'x', easing: expoInOut, x: 70 }}
                class="absolute -bottom-1 left-0 w-full h-1 bg-black"
            ></div>
        {/if}
    </div>
    <div
        role="button"
        tabindex="0"
        aria-pressed={selectedLanguage === reversalLanguage}
+       aria-label={`Switch to ${reversalLanguage} language`}
        on:click={() => onSwitchLanguage(reversalLanguage)}
        on:keydown={(e) => e.key === 'Enter' && onSwitchLanguage(reversalLanguage)}
        class="py-2.5 px-3.5 text-sm uppercase text-center relative dy-tabs dy-tabs-bordered mb-1"
    >
        {reversalLanguage}
        {#if selectedLanguage === reversalLanguage}
            <div
-               transition:fly={{ axis: 'x', easing: expoInOut, x: -70 }}
+               transition:fly={{ axis: 'x', easing: expoInOut, x: -70 }}
                class="absolute -bottom-1 left-0 w-full h-1 bg-black"
            ></div>
        {/if}
    </div>
    <div class="flex-1"></div>
</div>
src/lib/components/LexiconXMLView.svelte (1)

150-156: Guard against missing singleEntryStyles

config.singleEntryStyles is optional. If the dictionary has no styles, the for…of loop crashes.

-        for (let stl of config.singleEntryStyles) {
+        if (!config.singleEntryStyles) return;
+        for (let stl of config.singleEntryStyles) {
src/routes/lexicon/+page.ts (3)

36-40: Data shape of reversalAlphabets / reversalLanguages is awkward

Each element is { [key]: value }, producing an array of singleton objects:

[ { 'fr': [...] }, { 'es': [...] } ]

Down‑stream consumers must Object.values(obj)[0] every time.
Either:

  1. Keep them as true maps { [key: string]: alphabet }, or
  2. Return an array of tuples Array<{ lang: string; alphabet: string[] }>

Pick whichever matches the consuming component; otherwise this will cause unnecessary iteration and brittle as any casts.


70-86: First‑letter extraction can overrun the string

entry.name.substring(startingPosition, 2 + startingPosition) will produce a substring of variable length ≥ 2 when the word is only one character long, but you later test vernacularAlphabet.includes(firstTwoChars) which expects a single letter or digraph present in the alphabet list.

Use slice(start, start + 2) (which clamps) and normalise length:

-            firstTwoChars = entry.name
-                .substring(startingPosition, 2 + startingPosition)
-                .toLowerCase();
+            firstTwoChars = entry.name
+                .slice(startingPosition, startingPosition + 2)
+                .toLowerCase();

98-103: Return type lacks vernacular language & words

load() sets the stores but the returned data object omits them.
Including them allows page components to access via load props without subscribing to the stores again:

     return {
         vernacularAlphabet,
+        vernacularLanguage,
+        vernacularWords: vernacularWordsList,
         reversalAlphabets,
         reversalLanguages,
         reversalIndexes
     };
src/lib/components/WordNavigationStrip.svelte (1)

60-74: Normalise property names to prevent downstream type errors

The vernacular branch sets index, whereas the reversal branch sets indexes. Call‑sites need to handle both spellings, causing brittle code (see similar bug in search & lexicon pages). Consider standardising on one key (e.g., always expose indexes, even for a single ID).

Also applies to: 80-96

src/routes/lexicon/search/+page.svelte (2)

50-52: Remove noisy console logging or wrap in debug flag

The console.log(wordIds) statement will spam production logs. Either gate it behind a debug flag or remove it before shipping.


103-113: UI duplication: Search form & “No results” render together

When a search returns zero matches, both the form and the No results found message are shown. Consider hiding the form to keep the feedback concise or placing the empty‑state message inside the form component.

src/routes/lexicon/+page.svelte (1)

210-238: Scroll handler can become O(n²) with many letters

checkIfScrolledToBottom queries all [id^="letter-"] elements on every scroll event, which can be expensive for large lexicons. Consider throttling the handler and caching the NodeList.

🛑 Comments failed to post (17)
src/lib/components/AlphabetStrip.svelte (1)

1-24: 🛠️ Refactor suggestion

Good implementation with room for accessibility improvements.

The component is well-structured with responsive design, snap scrolling for touch devices, and clear visual feedback for the active letter. The design scales appropriately across different screen sizes.

Consider these accessibility enhancements:

  1. Add ARIA attributes for screen readers
  2. Implement keyboard navigation support
 <button
     class="px-3 py-2 text-sm font-bold border rounded-md bg-gray-100 hover:bg-gray-200 cursor-pointer snap-start
     sm:px-4 sm:py-3 sm:text-base
     md:px-5 md:py-4 md:text-base
     lg:px-6 lg:py-4 lg:text-lg"
     style={activeLetter === letter
         ? 'background-color: var(--TitleBackgroundColor); border-color: black;'
         : ''}
     on:click={() => onLetterSelect(letter)}
+    aria-current={activeLetter === letter ? 'true' : undefined}
+    aria-label={`Navigate to letter ${letter}`}
 >
     {letter}
 </button>
📝 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.

<script lang="ts">
    export let alphabet: string[];
    export let activeLetter: string;
    export let onLetterSelect: (letter: string) => void;
</script>

<div
    class="flex m-2 gap-1 md:gap-2 mb-4 justify-start overflow-x-auto whitespace-nowrap pb-2 snap-x min-w-[100vw]"
>
    {#each alphabet as letter}
        <button
            class="px-3 py-2 text-sm font-bold border rounded-md bg-gray-100 hover:bg-gray-200 cursor-pointer snap-start
            sm:px-4 sm:py-3 sm:text-base
            md:px-5 md:py-4 md:text-base
            lg:px-6 lg:py-4 lg:text-lg"
            style={activeLetter === letter
                ? 'background-color: var(--TitleBackgroundColor); border-color: black;'
                : ''}
            on:click={() => onLetterSelect(letter)}
            aria-current={activeLetter === letter ? 'true' : undefined}
            aria-label={`Navigate to letter ${letter}`}
        >
            {letter}
        </button>
    {/each}
</div>
src/routes/quiz/[collection]/[id]/+page.js (1)

10-14: 🛠️ Refactor suggestion

Validate that writing systems exist before destructuring

The code assumes that Object.entries(config.writingSystems) returns at least one entry. If config.writingSystems is an empty object, this will lead to undefined values.

- const [defaultKey, writingSystem] = Object.entries(config.writingSystems)[0];
+ const entries = Object.entries(config.writingSystems);
+ if (entries.length === 0) {
+     throw new Error('No writing systems found in configuration');
+ }
+ const [defaultKey, writingSystem] = entries[0];
📝 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 entries = Object.entries(config.writingSystems);
    if (entries.length === 0) {
        throw new Error('No writing systems found in configuration');
    }
    const [defaultKey, writingSystem] = entries[0];

    if (!writingSystem?.alphabet) {
        throw new Error('Writing system alphabet not found');
    }
src/lib/components/SearchForm.svelte (1)

16-24: 🛠️ Refactor suggestion

Add type safety for writingSystems access

The code accesses properties of writingSystems objects without type checking, which could lead to runtime errors.

let specialCharacters = [];
if (config.programType == 'SAB') {
    specialCharacters =
        config.mainFeatures['input-buttons']?.split(' ').filter((c) => c.length) ?? [];
} else if (config.programType === 'DAB') {
+   // Type-safe access to writing system properties
    specialCharacters = Object.values(config.writingSystems ?? {})
-       .filter((ws: any) => ws.type && ws.type.includes('main'))
-       .flatMap((ws: any) => ws.inputButtons || []);
+       .filter((ws) => ws?.type && Array.isArray(ws.type) && ws.type.includes('main'))
+       .flatMap((ws) => Array.isArray(ws.inputButtons) ? ws.inputButtons : []);
}
📝 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.

    let specialCharacters = [];
    if (config.programType == 'SAB') {
        specialCharacters =
            config.mainFeatures['input-buttons']?.split(' ').filter((c) => c.length) ?? [];
    } else if (config.programType === 'DAB') {
        // Type-safe access to writing system properties
        specialCharacters = Object.values(config.writingSystems ?? {})
            .filter((ws) => ws?.type && Array.isArray(ws.type) && ws.type.includes('main'))
            .flatMap((ws) => Array.isArray(ws.inputButtons) ? ws.inputButtons : []);
    }
src/lib/components/LexiconLanguageTabs.svelte (2)

1-9: 🛠️ Refactor suggestion

Add TypeScript type definitions for the component props

The component is missing TypeScript type definitions for its props, which would improve type safety and code documentation.

-<script>
+<script lang="ts">
    import { expoInOut } from 'svelte/easing';
    import { fly } from 'svelte/transition';

-   export let reversalLanguage;
-   export let selectedLanguage;
-   export let onSwitchLanguage;
-   export let vernacularLanguage;
+   export let reversalLanguage: string;
+   export let selectedLanguage: string;
+   export let onSwitchLanguage: (language: string) => void;
+   export let vernacularLanguage: string;
</script>
📝 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.

<script lang="ts">
    import { expoInOut } from 'svelte/easing';
    import { fly } from 'svelte/transition';

    export let reversalLanguage: string;
    export let selectedLanguage: string;
    export let onSwitchLanguage: (language: string) => void;
    export let vernacularLanguage: string;
</script>

1-45: 💡 Verification agent

❓ Verification inconclusive

Add validation for duplicate language identifiers

The component should handle the case where vernacularLanguage and reversalLanguage might be the same, which could lead to confusing UI behavior.


🏁 Script executed:

#!/bin/bash
# Check if there are cases where vernacularLanguage and reversalLanguage might be the same
rg -r '$1' -g "*.svelte" -g "*.ts" -g "*.js" "LexiconLanguageTabs.*vernacularLanguage=\"?(\w+)\"?.*reversalLanguage=\"?(\w+)\"?" --replace '$1 $2' | grep -P '(\w+) \1'

Length of output: 165


Validate unique language props in LexiconLanguageTabs

Currently, if vernacularLanguage and reversalLanguage are identical, both tabs render the same label—which can confuse users. Add a runtime check at the top of src/lib/components/LexiconLanguageTabs.svelte to guard against this:

• File to update:

  • src/lib/components/LexiconLanguageTabs.svelte

• Suggested change:

<script>
  export let vernacularLanguage;
  export let reversalLanguage;
  export let selectedLanguage;
  export let onSwitchLanguage;

  // Validate that the two languages differ
  if (vernacularLanguage === reversalLanguage) {
    throw new Error(
      'vernacularLanguage and reversalLanguage must be distinct'
    );
  }
</script>

You can also choose to emit a console warning or render a single tab instead of throwing—whatever best fits the UX requirements.

src/lib/components/LexiconXMLView.svelte (4)

125-148: 🛠️ Refactor suggestion

afterUpdate & cloning logic can create an infinite fetch / listener loop

  1. afterUpdate calls updateXmlData() unconditionally; updateXmlData sets xmlData, causing another update → endless network & DOM churn.
    Use a reactive statement or onMount watcher keyed to wordIds instead.

  2. Detaching listeners by cloning every .clickable node forces a complete DOM rebuild each update. Consider:

    <span
      class="clickable"
      on:click={() => handleClick(word, index, homonym)}
    >

    to rely on Svelte’s diffing instead of manual cloning.

These two issues combined will seriously hurt performance on every keystroke in the search page.


68-77: 🛠️ Refactor suggestion

Potential NPE & stale data when looking up vernacular words

get(vernacularWordsStore) can be undefined the first time this runs, and the store’s static typing is string[], even though you later treat each item as an object with id, name, homonym_index.
Update the store’s generic (in stores/lexicon.ts) and guard for an empty store here.

-        const wordObject = get(vernacularWordsStore).find(
+        const words = get(vernacularWordsStore) ?? [];
+        const wordObject = words.find(
             (item) => item.id === index
         );
📝 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 href = node.getAttribute('href');
                    const match = href.match(/E-(\d+)/); // Extract index number
                    if (match) {
                        const index = parseInt(match[1], 10); // Extracted number as integer
                        const words = get(vernacularWordsStore) ?? [];
                        const wordObject = words.find(
                            (item) => item.id === index
                        );
                        const word = wordObject ? wordObject.name : 'Unknown'; // Fallback if not found
                        const homonymIndex = wordObject ? wordObject.homonym_index : 1; // Default to 1 if not found

18-30: 💡 Verification agent

❓ Verification inconclusive

queryXmlByWordId returns rows, not raw XML strings

db.exec returns an array of result objects, where each values element is itself an array containing one cell per column.
return results[0].values therefore produces e.g. [ [ '<entry>…</entry>' ], [ '<entry>…</entry>' ], … ].
Down‑stream, updateXmlData treats every element as a raw string and calls formatXmlByClass(xml), which will receive an array and explode in DOMParser.

-            return results[0].values;
+            // Flatten to a simple array of xml strings
+            return results[0].values.map((row) => row[0]);

Also remove the debug console.log before merging.


Flatten returned XML rows in queryXmlByWordId and remove debug log

  • In src/lib/components/LexiconXMLView.svelte (around lines 18–30):
    • Remove the debug log:
    -    console.log('results:', results[0].values);
    • Replace the raw return with a flattened array of strings:
    -    return results[0].values;
    +    // Flatten to a simple array of xml strings
    +    return results[0].values.map(row => row[0]);

This ensures updateXmlData (and its call to formatXmlByClass(xml)) receives plain XML strings, not nested arrays.

📝 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.

    async function queryXmlByWordId(wordIds) {
        try {
            let db = await initializeDatabase({ fetch });

            let results;
            const dynamicQuery = wordIds.map(() => `id = ?`).join(' OR ');
            const dynamicParams = wordIds.map((id) => id);
            results = db.exec(`SELECT xml FROM entries WHERE ${dynamicQuery}`, dynamicParams);

            // Flatten to a simple array of xml strings
            return results[0].values.map((row) => row[0]);
        } catch (error) {
            console.error(`Error querying XML for word IDs ${wordIds}:`, error);

48-66: ⚠️ Potential issue

processNode may dereference null parents

For the root element node.parentNode is null, so node.parentNode.children throws.
Guard before use:

-                    [...node.parentNode.children].some(
+                    (node.parentNode && [...node.parentNode.children].some(
                         (child) =>
                             child.getAttribute &&
                             (child.getAttribute('class') || '').includes('sensenumber')
-                    );
+                    ));
📝 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.

        function processNode(node, parentHasSenseNumber = false) {
            let output = '';

            if (node.nodeType === Node.TEXT_NODE) {
                return node.nodeValue.trim() ? node.nodeValue + ' ' : '';
            }

            if (node.nodeType === Node.ELEMENT_NODE) {
                let className = node.getAttribute('class') || '';
                let isSenseNumber = className.includes('sensenumber');

                let parentContainsSenseNumber =
                    parentHasSenseNumber ||
                    (node.parentNode && [...node.parentNode.children].some(
                        (child) =>
                            child.getAttribute &&
                            (child.getAttribute('class') || '').includes('sensenumber')
                    ));
src/lib/components/LexiconReversalView.svelte (1)

20-25: 🛠️ Refactor suggestion

Reactive blocks continuously reset currentLetter

Every change to alphabet or selectedLetter triggers the two $: assignments, overwriting a user’s in‑progress selection:

$: if (alphabet && alphabet.length > 0) {
    currentLetter = alphabet[0];   // ← keeps firing
}

Combine the two blocks and guard:

-$: if (alphabet && alphabet.length > 0) {
-    currentLetter = alphabet[0];
-}
-$: if (selectedLetter !== currentLetter) {
-    currentLetter = selectedLetter;
-}
+$: {
+    if (selectedLetter) {
+        currentLetter = selectedLetter;
+    } else if (alphabet?.length) {
+        currentLetter = alphabet[0];
+    }
+}

This prevents accidental resets and honours the controlled selectedLetter prop.

src/routes/lexicon/+page.ts (2)

54-60: 🛠️ Refactor suggestion

Type mismatch: vernacularWordsStore declared as string[] but receives objects

stores/lexicon.ts:

export const vernacularWordsStore = writable<string[]>();

Here you push objects with { id, name, … }.
Update the store type to an explicit interface so TypeScript (and IDE tooling) can help:

-export const vernacularWordsStore = writable<string[]>();
+export interface VernacularEntry {
+  id: number;
+  name: string;
+  homonym_index: number;
+  type: string;
+  num_senses: number;
+  summary: string;
+  letter: string;
+}
+export const vernacularWordsStore = writable<VernacularEntry[]>();

Then audit usages (LexiconXMLView etc.) to remove any casts.


32-34: ⚠️ Potential issue

reversalWritingSystems length should be checked, not truthiness

filter always returns an array; if (!reversalWritingSystems) will never execute.
Use a length check to throw when none are configured:

-if (!reversalWritingSystems) {
+if (reversalWritingSystems.length === 0) {
     throw new Error('Reversal language not found');
 }
📝 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.

    if (reversalWritingSystems.length === 0) {
        throw new Error('Reversal language not found');
    }
src/lib/components/WordNavigationStrip.svelte (2)

16-22: ⚠️ Potential issue

Make wordsList reactive instead of a one–time snapshot

get() only captures the stores’ values at component instantiation, so wordsList will never update when the language or the underlying lists change (e.g., after switching tabs or when the store is populated asynchronously). Convert the computation to a reactive statement that re‑evaluates whenever either store updates.

-// List of all words (should come from either vernacularWordsList or reversalWordsList)
-let wordsList;
-if (get(selectedLanguageStore) === get(vernacularLanguageStore)) {
-    wordsList = get(vernacularWordsStore);
-} else {
-    wordsList = get(currentReversalWordsStore);
-}
+// Reactively choose the correct word list
+$: wordsList =
+    $selectedLanguageStore === $vernacularLanguageStore
+        ? $vernacularWordsStore
+        : $currentReversalWordsStore;

24-56: 🛠️ Refactor suggestion

Guard against currentIndex === -1 to avoid incorrect navigation

When the current word is not found, currentIndex becomes ‑1, which makes nextWord point to wordsList[0] (the first word) and leaves previousWord null, producing a confusing UI jump. Handle the sentinel value explicitly.

-$: previousWord = currentIndex > 0 ? wordsList[currentIndex - 1] : null;
-$: nextWord = currentIndex < wordsList.length - 1 ? wordsList[currentIndex + 1] : null;
+$: {
+    previousWord = currentIndex > 0 ? wordsList[currentIndex - 1] : null;
+    nextWord =
+        currentIndex !== -1 && currentIndex < wordsList.length - 1
+            ? wordsList[currentIndex + 1]
+            : null;
+}
📝 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.

    // Compute previous and next words
    $: {
        previousWord = currentIndex > 0
            ? wordsList[currentIndex - 1]
            : null;
        nextWord =
            currentIndex !== -1 && currentIndex < wordsList.length - 1
                ? wordsList[currentIndex + 1]
                : null;
    }
src/routes/lexicon/search/+page.svelte (1)

54-57: ⚠️ Potential issue

Null‑selection path throws a runtime error

If the same word is clicked twice, selectedWord becomes null, yet the next line dereferences it (selectedWord.indexes). This raises TypeError: cannot read properties of null.

-function selectWord(word) {
-    selectedWord = selectedWord && selectedWord.word === word ? null : word;
-    wordIds = selectedWord.indexes ? selectedWord.indexes : [selectedWord.index];
+function selectWord(word) {
+    const newSelection =
+        selectedWord && selectedWord.word === word ? null : word;
+    selectedWord = newSelection;
+
+    if (selectedWord) {
+        wordIds = selectedWord.indexes ?? [selectedWord.index];
+    } else {
+        wordIds = [];
+    }
 }
📝 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.

    function selectWord(word) {
        const newSelection =
            selectedWord && selectedWord.word === word ? null : word;
        selectedWord = newSelection;

        if (selectedWord) {
            wordIds = selectedWord.indexes ?? [selectedWord.index];
        } else {
            wordIds = [];
        }
    }
src/lib/data/stores/lexicon.ts (2)

48-69: 🛠️ Refactor suggestion

Possible double‑initialisation race

Multiple callers invoking initializeDatabase simultaneously will all run the fetch & init logic because the guard only checks current values, not a pending promise. Use a shared promise or lock to ensure the work happens once.


8-10: ⚠️ Potential issue

Incorrect store type for vernacular words

vernacularWordsStore holds objects, not strings, so writable<string[]>() is misleading and loses type safety.

-export const vernacularWordsStore = writable<string[]>();
+export interface VernacularWord {
+    id: string;
+    name: string;
+    homonym_index?: number;
+}
+
+export const vernacularWordsStore = writable<VernacularWord[]>([]);
📝 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.

// Store for vernacularWordsList
export interface VernacularWord {
    id: string;
    name: string;
    homonym_index?: number;
}

export const vernacularWordsStore = writable<VernacularWord[]>([]);

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: 11

🧹 Nitpick comments (6)
src/lib/components/LexiconXMLView.svelte (1)

151-155: singleEntryStyles may be undefined

Accessing for (let stl of config.singleEntryStyles) will crash when the property is absent. Add a guard:

-        for (let stl of config.singleEntryStyles) {
+        if (!config.singleEntryStyles) return;
+        for (let stl of config.singleEntryStyles) {
src/routes/lexicon/+page.ts (1)

32-34: Ineffective reversal‑language presence check

reversalWritingSystems is always an array; the current truthiness test never fails. Use a length check instead:

-    if (!reversalWritingSystems) {
+    if (reversalWritingSystems.length === 0) {
         throw new Error('Reversal language not found');
     }
src/routes/lexicon/+page.svelte (4)

35-35: Use reactive declaration for derived value

The showBackButton variable can be simplified to a reactive declaration.

-let selectedWord = null;
-$: showBackButton = selectedWord ? true : false;
+let selectedWord = null;
+$: showBackButton = !!selectedWord;

310-325: Refactor duplicate scroll container code

The scroll container div is duplicated across three different conditional branches with the same classes and event handlers.

Consider refactoring the three similar scroll container divs into a single component or at least into a variable template:

{#if selectedWord}
    <WordNavigationStrip currentWord={selectedWord} onSelectWord={selectWord} />
    <div
        class="flex-1 overflow-y-auto bg-base-100 width-full"
        bind:this={scrollContainer}
        on:scroll={checkIfScrolledToBottom}
    >
        <LexiconXmlView {wordIds} onSelectWord={selectWord} />
    </div>
{:else}
    <div
        class="flex-1 overflow-y-auto bg-base-100 width-full"
        bind:this={scrollContainer}
        on:scroll={checkIfScrolledToBottom}
    >
        {#if selectedLanguage === vernacularLanguage}
            <LexiconVernacularListView {vernacularWordsList} onSelectWord={selectWord} />
        {:else}
            <LexiconReversalListView {reversalWordsList} onSelectWord={selectWord} />
        {/if}
    </div>
{/if}

60-61: Remove console.log statement

Console.log statements should not be present in production code.

 if (selectedLanguage === reversalLanguage && !loadedReversalLetters.has(letter)) {
-    console.log('Loading letter data:', letter);
+    // Loading letter data

241-242: Improve reactive statement readability

The reactive statement for currentAlphabet can be made more readable.

-$: currentAlphabet =
-    selectedLanguage === reversalLanguage ? alphabets.reversal : alphabets.vernacular;
+$: currentAlphabet = selectedLanguage === reversalLanguage 
+    ? alphabets.reversal 
+    : alphabets.vernacular;
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8115092 and 4328c85.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (23)
  • config/index.d.ts (1 hunks)
  • convert/convertConfig.ts (1 hunks)
  • convert/convertReverseIndex.ts (4 hunks)
  • convert/tests/sab/convertConfigSAB.test.ts (1 hunks)
  • package.json (1 hunks)
  • src/lib/components/AlphabetStrip.svelte (1 hunks)
  • src/lib/components/LexiconLanguageTabs.svelte (1 hunks)
  • src/lib/components/LexiconReversalListView.svelte (1 hunks)
  • src/lib/components/LexiconReversalView.svelte (1 hunks)
  • src/lib/components/LexiconVernacularListView.svelte (1 hunks)
  • src/lib/components/LexiconXMLView.svelte (1 hunks)
  • src/lib/components/SearchForm.svelte (4 hunks)
  • src/lib/components/Sidebar.svelte (2 hunks)
  • src/lib/components/WordNavigationStrip.svelte (1 hunks)
  • src/lib/data/stores/lexicon.ts (1 hunks)
  • src/lib/lexicon/index.ts (1 hunks)
  • src/lib/search-worker/dab-search-worker.ts (1 hunks)
  • src/lib/search/domain/interfaces/data-interfaces.ts (1 hunks)
  • src/routes/+layout.svelte (1 hunks)
  • src/routes/lexicon/+page.svelte (1 hunks)
  • src/routes/lexicon/+page.ts (1 hunks)
  • src/routes/lexicon/search/+page.svelte (1 hunks)
  • src/routes/quiz/[collection]/[id]/+page.js (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • src/routes/+layout.svelte
🚧 Files skipped from review as they are similar to previous changes (18)
  • convert/tests/sab/convertConfigSAB.test.ts
  • src/lib/components/Sidebar.svelte
  • src/lib/search/domain/interfaces/data-interfaces.ts
  • package.json
  • src/lib/components/LexiconReversalListView.svelte
  • src/lib/components/AlphabetStrip.svelte
  • convert/convertReverseIndex.ts
  • config/index.d.ts
  • src/lib/components/LexiconReversalView.svelte
  • convert/convertConfig.ts
  • src/lib/search-worker/dab-search-worker.ts
  • src/lib/lexicon/index.ts
  • src/lib/components/SearchForm.svelte
  • src/lib/components/LexiconVernacularListView.svelte
  • src/lib/components/LexiconLanguageTabs.svelte
  • src/routes/lexicon/search/+page.svelte
  • src/lib/components/WordNavigationStrip.svelte
  • src/lib/data/stores/lexicon.ts
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/routes/lexicon/+page.ts (3)
config/index.d.ts (1)
  • DictionaryConfig (279-295)
src/lib/lexicon/index.ts (1)
  • ReversalIndex (1-3)
src/lib/data/stores/lexicon.ts (3)
  • initializeDatabase (48-69)
  • vernacularLanguageStore (6-6)
  • vernacularWordsStore (9-9)
🔇 Additional comments (3)
src/routes/quiz/[collection]/[id]/+page.js (1)

5-15: Object.entries(...)[0] is nondeterministic

JavaScript object enumeration order isn’t guaranteed to match user expectations. Relying on “first entry” to pick a default writing system can yield random results across browsers or future config changes.

Consider picking the writing system flagged as default (e.g., ws.type.includes('main')) or introduce an explicit defaultWritingSystem key in the configuration.

src/routes/lexicon/+page.ts (2)

36-40: Data shape mismatch may confuse consumers

reversalAlphabets and reversalLanguages are arrays of single‑key objects ([{ en: [...] }, { fr: [...] }]).
Callers typically expect a plain map ({ en: [...], fr: [...] }). Confirm the expected shape or refactor:

-const reversalAlphabets = reversalWritingSystems.map(([key, ws]) => ({ [key]: ws.alphabet }));
+const reversalAlphabets = Object.fromEntries(
+    reversalWritingSystems.map(([key, ws]) => [key, ws.alphabet])
+);

69-89: ⚠️ Potential issue

entry.name.substring(startingPosition, 2 + startingPosition) is off‑by‑one

substring’s second argument is an end index, not a length. For two characters you need startingPosition + 2, not 2 + startingPosition (works only when startingPosition ≤ 1). Extracting from offset 1 would currently yield one char.

-            firstTwoChars = entry.name
-                .substring(startingPosition, 2 + startingPosition)
+            firstTwoChars = entry.name
+                .substring(startingPosition, startingPosition + 2)

Likely an incorrect or invalid review comment.

Comment on lines 89 to 99
output += '<' + node.tagName;
for (let attr of node.attributes) {
output += ` ${attr.name}="${attr.value}"`;
}
output += '>';

for (let child of node.childNodes) {
output += processNode(child, parentContainsSenseNumber || isSenseNumber);
}

output += `</${node.tagName}>`;
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Potential XSS: arbitrary XML is injected as raw HTML

All element names & attribute values from the DB are retransmitted verbatim into xmlData, which is later rendered with {@html …}.
A malicious entry containing <script> or an inline event handler (onerror="steal()") will execute client‑side, compromising the app.

Mitigate by whitelisting tags/attributes or sanitising with a library such as DOMPurify before returning the string.

Comment on lines +18 to +33
async function queryXmlByWordId(wordIds) {
try {
let db = await initializeDatabase({ fetch });

let results;
const dynamicQuery = wordIds.map(() => `id = ?`).join(' OR ');
const dynamicParams = wordIds.map((id) => id);
results = db.exec(`SELECT xml FROM entries WHERE ${dynamicQuery}`, dynamicParams);
console.log('results:', results[0].values);

return results[0].values;
} catch (error) {
console.error(`Error querying XML for word IDs ${wordIds}:`, error);
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

Guard against empty‐result crashes & remove noisy logging

db.exec returns an empty array when no rows match. Accessing results[0].values will then throw TypeError: Cannot read properties of undefined.
Additionally, the console.log leaks internal data and can flood the console in production.

-            results = db.exec(`SELECT xml FROM entries WHERE ${dynamicQuery}`, dynamicParams);
-            console.log('results:', results[0].values);
-
-            return results[0].values;
+            results = db.exec(
+                `SELECT xml FROM entries WHERE ${dynamicQuery}`,
+                dynamicParams
+            );
+
+            if (!results.length) {
+                return [];               // ← safe fallback
+            }
+
+            return results[0].values;
📝 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.

Suggested change
async function queryXmlByWordId(wordIds) {
try {
let db = await initializeDatabase({ fetch });
let results;
const dynamicQuery = wordIds.map(() => `id = ?`).join(' OR ');
const dynamicParams = wordIds.map((id) => id);
results = db.exec(`SELECT xml FROM entries WHERE ${dynamicQuery}`, dynamicParams);
console.log('results:', results[0].values);
return results[0].values;
} catch (error) {
console.error(`Error querying XML for word IDs ${wordIds}:`, error);
return null;
}
}
async function queryXmlByWordId(wordIds) {
try {
let db = await initializeDatabase({ fetch });
let results;
const dynamicQuery = wordIds.map(() => `id = ?`).join(' OR ');
const dynamicParams = wordIds.map((id) => id);
results = db.exec(
`SELECT xml FROM entries WHERE ${dynamicQuery}`,
dynamicParams
);
if (!results.length) {
return []; // ← safe fallback
}
return results[0].values;
} catch (error) {
console.error(`Error querying XML for word IDs ${wordIds}:`, error);
return null;
}
}

Comment on lines +126 to +148
const spans = document.querySelectorAll('.clickable');

spans.forEach((span) => {
const oldSpan = span.cloneNode(true);
span.parentNode.replaceChild(oldSpan, span);
});

const freshSpans = document.querySelectorAll('.clickable');
freshSpans.forEach((span) => {
span.addEventListener('click', () => {
selectedLanguageStore.set(get(vernacularLanguageStore));
const word = span.getAttribute('data-word');
const index = parseInt(span.getAttribute('data-index'), 10);
const homonym_index = parseInt(span.getAttribute('data-homonym'), 10);

onSelectWord({
word,
index,
homonym_index
});
});
});
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Inefficient DOM manipulation & event re‑binding

Cloning every .clickable span on each update is O(n²) over time and breaks browser DevTools event tracing.
Prefer event delegation once on a stable parent node:

-function attachEventListeners() {
-    const spans = document.querySelectorAll('.clickable');
-
-    freshSpans.forEach((span) => {
-        span.addEventListener('click', () => {
-
-        });
-    });
-}
+function attachEventListeners() {
+    const container = document.getElementById('xml-container');
+    if (!container) return;
+    container.addEventListener('click', (e) => {
+        const span = e.target.closest('.clickable');
+        if (!span) return;
+        selectedLanguageStore.set(get(vernacularLanguageStore));
+        onSelectWord({
+            word: span.dataset.word,
+            index: Number(span.dataset.index),
+            homonym_index: Number(span.dataset.homonym)
+        });
+    });
+}

(You’ll need to wrap the rendered HTML in an element with id="xml-container".)

📝 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.

Suggested change
const spans = document.querySelectorAll('.clickable');
spans.forEach((span) => {
const oldSpan = span.cloneNode(true);
span.parentNode.replaceChild(oldSpan, span);
});
const freshSpans = document.querySelectorAll('.clickable');
freshSpans.forEach((span) => {
span.addEventListener('click', () => {
selectedLanguageStore.set(get(vernacularLanguageStore));
const word = span.getAttribute('data-word');
const index = parseInt(span.getAttribute('data-index'), 10);
const homonym_index = parseInt(span.getAttribute('data-homonym'), 10);
onSelectWord({
word,
index,
homonym_index
});
});
});
}
function attachEventListeners() {
const container = document.getElementById('xml-container');
if (!container) return;
container.addEventListener('click', (e) => {
const span = e.target.closest('.clickable');
if (!span) return;
selectedLanguageStore.set(get(vernacularLanguageStore));
onSelectWord({
word: span.dataset.word,
index: Number(span.dataset.index),
homonym_index: Number(span.dataset.homonym)
});
});
}

Comment on lines 20 to 26
const response = await fetch(`${base}/reversal/language/${defaultKey}/${alphabet[0]}.json`);
if (response.ok) {
initialReversalData = await response.json();
}
} catch (error) {
console.error('Error loading initial reversal data:', error);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

404‑prone hard‑coded URL segment

The fetch path contains /reversal/language/${defaultKey}/…, whereas other code (e.g., lexicon loader) uses /reversal/${key}/index.json. Verify that the extra language segment exists on the server; otherwise the request will silently fail and initialReversalData remains {}.

Comment on lines 186 to 196
function switchLanguage(language) {
selectedLanguageStore.set(language);
selectedLetter = currentAlphabet[0];
if (selectedLanguage != vernacularLanguage) {
fetchWords();
}
const scrollableDiv = document.querySelector('.flex-1.overflow-y-auto.bg-base-100');
if (scrollableDiv) {
scrollableDiv.scrollTop = 0;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Fix non-strict comparison and use Svelte binding for DOM access

The function is using a non-strict comparison and directly accessing the DOM with querySelector instead of using Svelte bindings.

 function switchLanguage(language) {
     selectedLanguageStore.set(language);
     selectedLetter = currentAlphabet[0];
-    if (selectedLanguage != vernacularLanguage) {
+    if (selectedLanguage !== vernacularLanguage) {
         fetchWords();
     }
-    const scrollableDiv = document.querySelector('.flex-1.overflow-y-auto.bg-base-100');
-    if (scrollableDiv) {
-        scrollableDiv.scrollTop = 0;
-    }
+    if (scrollContainer) {
+        scrollContainer.scrollTop = 0;
+    }
 }
📝 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.

Suggested change
function switchLanguage(language) {
selectedLanguageStore.set(language);
selectedLetter = currentAlphabet[0];
if (selectedLanguage != vernacularLanguage) {
fetchWords();
}
const scrollableDiv = document.querySelector('.flex-1.overflow-y-auto.bg-base-100');
if (scrollableDiv) {
scrollableDiv.scrollTop = 0;
}
}
function switchLanguage(language) {
selectedLanguageStore.set(language);
selectedLetter = currentAlphabet[0];
if (selectedLanguage !== vernacularLanguage) {
fetchWords();
}
if (scrollContainer) {
scrollContainer.scrollTop = 0;
}
}

Comment on lines 58 to 88
async function fetchWords(letter = selectedLetter) {
if (selectedLanguage === reversalLanguage && !loadedReversalLetters.has(letter)) {
console.log('Loading letter data:', letter);

const letterIndex = alphabets.reversal.indexOf(letter);
const lettersToLoad = alphabets.reversal
.slice(0, letterIndex)
.filter((l) => !loadedReversalLetters.has(l));

// Load all required letters in parallel
await Promise.all(lettersToLoad.map(loadLetterData));

// Load the current letter
await loadLetterData(letter);

// Sort the results based on the selectedLanguage's alphabet
reversalWordsStore.update((words) => {
const updatedWords = { ...words };
updatedWords[selectedLanguage] = (updatedWords[selectedLanguage] || []).sort(
(a, b) => {
const alphabet = currentAlphabet;
return (
alphabet.indexOf(a.word[0].toLowerCase()) -
alphabet.indexOf(b.word[0].toLowerCase())
);
}
);
return updatedWords;
});
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add error handling to fetchWords function

The fetchWords function lacks error handling for potential failures during letter data loading. If any of the letter data loading operations fail, the function will throw an unhandled exception, potentially breaking the UI.

 async function fetchWords(letter = selectedLetter) {
     if (selectedLanguage === reversalLanguage && !loadedReversalLetters.has(letter)) {
         console.log('Loading letter data:', letter);

         const letterIndex = alphabets.reversal.indexOf(letter);
         const lettersToLoad = alphabets.reversal
             .slice(0, letterIndex)
             .filter((l) => !loadedReversalLetters.has(l));

         // Load all required letters in parallel
+        try {
             await Promise.all(lettersToLoad.map(loadLetterData));
 
             // Load the current letter
             await loadLetterData(letter);
 
             // Sort the results based on the selectedLanguage's alphabet
             reversalWordsStore.update((words) => {
                 const updatedWords = { ...words };
                 updatedWords[selectedLanguage] = (updatedWords[selectedLanguage] || []).sort(
                     (a, b) => {
                         const alphabet = currentAlphabet;
                         return (
                             alphabet.indexOf(a.word[0].toLowerCase()) -
                             alphabet.indexOf(b.word[0].toLowerCase())
                         );
                     }
                 );
                 return updatedWords;
             });
+        } catch (error) {
+            console.error('Error loading letter data:', error);
+            // Consider showing a user-friendly error message
+        }
     }
 }
📝 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.

Suggested change
async function fetchWords(letter = selectedLetter) {
if (selectedLanguage === reversalLanguage && !loadedReversalLetters.has(letter)) {
console.log('Loading letter data:', letter);
const letterIndex = alphabets.reversal.indexOf(letter);
const lettersToLoad = alphabets.reversal
.slice(0, letterIndex)
.filter((l) => !loadedReversalLetters.has(l));
// Load all required letters in parallel
await Promise.all(lettersToLoad.map(loadLetterData));
// Load the current letter
await loadLetterData(letter);
// Sort the results based on the selectedLanguage's alphabet
reversalWordsStore.update((words) => {
const updatedWords = { ...words };
updatedWords[selectedLanguage] = (updatedWords[selectedLanguage] || []).sort(
(a, b) => {
const alphabet = currentAlphabet;
return (
alphabet.indexOf(a.word[0].toLowerCase()) -
alphabet.indexOf(b.word[0].toLowerCase())
);
}
);
return updatedWords;
});
}
}
async function fetchWords(letter = selectedLetter) {
if (selectedLanguage === reversalLanguage && !loadedReversalLetters.has(letter)) {
console.log('Loading letter data:', letter);
const letterIndex = alphabets.reversal.indexOf(letter);
const lettersToLoad = alphabets.reversal
.slice(0, letterIndex)
.filter((l) => !loadedReversalLetters.has(l));
// Load all required letters in parallel
try {
await Promise.all(lettersToLoad.map(loadLetterData));
// Load the current letter
await loadLetterData(letter);
// Sort the results based on the selectedLanguage's alphabet
reversalWordsStore.update((words) => {
const updatedWords = { ...words };
updatedWords[selectedLanguage] = (updatedWords[selectedLanguage] || []).sort(
(a, b) => {
const alphabet = currentAlphabet;
return (
alphabet.indexOf(a.word[0].toLowerCase()) -
alphabet.indexOf(b.word[0].toLowerCase())
);
}
);
return updatedWords;
});
} catch (error) {
console.error('Error loading letter data:', error);
// Consider showing a user-friendly error message
}
}
}

Comment on lines 90 to 156
async function loadLetterData(letter) {
let newWords = [];

const index = reversalIndexes[defaultReversalKey];
const files = index[letter] || [];
for (const file of files) {
const reversalFile = `${base}/reversal/${defaultReversalKey}/${file}`;
const response = await fetch(reversalFile);
if (response.ok) {
const data = await response.json();
const currentFileWords = Object.entries(data).map(([word, entries]) => {
return {
word: word,
indexes: entries.map((entry) => entry.index),
vernacularWords: entries
.map((entry) => {
const foundWord = vernacularWordsList.find(
(vw) => vw.id === entry.index
);
if (foundWord) {
return {
name: foundWord.name,
homonymIndex: foundWord.homonym_index || 0
};
} else {
console.log(
`Index ${entry.index} not found in vernacularWordsList`
);
return null; // Return null for missing indexes
}
})
.filter((index) => index !== null), // Filter out null values
letter: letter
};
});

currentFileWords.forEach((newWord) => {
const existingWord = newWords.find((w) => w.word === newWord.word);
if (existingWord) {
existingWord.indexes = [
...new Set([...existingWord.indexes, ...newWord.indexes])
];
} else {
newWords.push(newWord);
}
});
}
}

reversalWordsStore.update((words) => {
const updatedWords = { ...words };
updatedWords[selectedLanguage] = [
...(updatedWords[selectedLanguage] || []),
...newWords
];
return updatedWords;
});

reversalLettersStore.update((letters) => {
const updatedLetters = { ...letters };
updatedLetters[selectedLanguage] = [
...(updatedLetters[selectedLanguage] || []),
letter
];
return updatedLetters;
});
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Improve error handling and remove console.log in loadLetterData

The function contains a console.log statement for debugging and lacks proper error handling for failed responses or potential issues with JSON parsing.

 async function loadLetterData(letter) {
     let newWords = [];

     const index = reversalIndexes[defaultReversalKey];
     const files = index[letter] || [];
     for (const file of files) {
         const reversalFile = `${base}/reversal/${defaultReversalKey}/${file}`;
+        try {
             const response = await fetch(reversalFile);
             if (response.ok) {
                 const data = await response.json();
                 const currentFileWords = Object.entries(data).map(([word, entries]) => {
                     return {
                         word: word,
                         indexes: entries.map((entry) => entry.index),
                         vernacularWords: entries
                             .map((entry) => {
                                 const foundWord = vernacularWordsList.find(
                                     (vw) => vw.id === entry.index
                                 );
                                 if (foundWord) {
                                     return {
                                         name: foundWord.name,
                                         homonymIndex: foundWord.homonym_index || 0
                                     };
                                 } else {
-                                    console.log(
-                                        `Index ${entry.index} not found in vernacularWordsList`
-                                    );
+                                    // Silently ignore missing indices or log to a dedicated logging system
                                     return null; // Return null for missing indexes
                                 }
                             })
                             .filter((index) => index !== null), // Filter out null values
                         letter: letter
                     };
                 });

                 currentFileWords.forEach((newWord) => {
                     const existingWord = newWords.find((w) => w.word === newWord.word);
                     if (existingWord) {
                         existingWord.indexes = [
                             ...new Set([...existingWord.indexes, ...newWord.indexes])
                         ];
                     } else {
                         newWords.push(newWord);
                     }
                 });
+            } else {
+                console.error(`Failed to fetch file: ${reversalFile}. Status: ${response.status}`);
+            }
+        } catch (error) {
+            console.error(`Error loading data for letter ${letter} from ${file}:`, error);
+        }
     }

     reversalWordsStore.update((words) => {
         const updatedWords = { ...words };
         updatedWords[selectedLanguage] = [
             ...(updatedWords[selectedLanguage] || []),
             ...newWords
         ];
         return updatedWords;
     });

     reversalLettersStore.update((letters) => {
         const updatedLetters = { ...letters };
         updatedLetters[selectedLanguage] = [
             ...(updatedLetters[selectedLanguage] || []),
             letter
         ];
         return updatedLetters;
     });
 }
📝 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.

Suggested change
async function loadLetterData(letter) {
let newWords = [];
const index = reversalIndexes[defaultReversalKey];
const files = index[letter] || [];
for (const file of files) {
const reversalFile = `${base}/reversal/${defaultReversalKey}/${file}`;
const response = await fetch(reversalFile);
if (response.ok) {
const data = await response.json();
const currentFileWords = Object.entries(data).map(([word, entries]) => {
return {
word: word,
indexes: entries.map((entry) => entry.index),
vernacularWords: entries
.map((entry) => {
const foundWord = vernacularWordsList.find(
(vw) => vw.id === entry.index
);
if (foundWord) {
return {
name: foundWord.name,
homonymIndex: foundWord.homonym_index || 0
};
} else {
console.log(
`Index ${entry.index} not found in vernacularWordsList`
);
return null; // Return null for missing indexes
}
})
.filter((index) => index !== null), // Filter out null values
letter: letter
};
});
currentFileWords.forEach((newWord) => {
const existingWord = newWords.find((w) => w.word === newWord.word);
if (existingWord) {
existingWord.indexes = [
...new Set([...existingWord.indexes, ...newWord.indexes])
];
} else {
newWords.push(newWord);
}
});
}
}
reversalWordsStore.update((words) => {
const updatedWords = { ...words };
updatedWords[selectedLanguage] = [
...(updatedWords[selectedLanguage] || []),
...newWords
];
return updatedWords;
});
reversalLettersStore.update((letters) => {
const updatedLetters = { ...letters };
updatedLetters[selectedLanguage] = [
...(updatedLetters[selectedLanguage] || []),
letter
];
return updatedLetters;
});
}
async function loadLetterData(letter) {
let newWords = [];
const index = reversalIndexes[defaultReversalKey];
const files = index[letter] || [];
for (const file of files) {
const reversalFile = `${base}/reversal/${defaultReversalKey}/${file}`;
try {
const response = await fetch(reversalFile);
if (response.ok) {
const data = await response.json();
const currentFileWords = Object.entries(data).map(([word, entries]) => {
return {
word: word,
indexes: entries.map((entry) => entry.index),
vernacularWords: entries
.map((entry) => {
const foundWord = vernacularWordsList.find(
(vw) => vw.id === entry.index
);
if (foundWord) {
return {
name: foundWord.name,
homonymIndex: foundWord.homonym_index || 0
};
} else {
// Silently ignore missing indices or log to a dedicated logging system
return null; // Return null for missing indexes
}
})
.filter((index) => index !== null), // Filter out null values
letter: letter
};
});
currentFileWords.forEach((newWord) => {
const existingWord = newWords.find((w) => w.word === newWord.word);
if (existingWord) {
existingWord.indexes = [
...new Set([...existingWord.indexes, ...newWord.indexes])
];
} else {
newWords.push(newWord);
}
});
} else {
console.error(`Failed to fetch file: ${reversalFile}. Status: ${response.status}`);
}
} catch (error) {
console.error(`Error loading data for letter ${letter} from ${file}:`, error);
}
}
reversalWordsStore.update((words) => {
const updatedWords = { ...words };
updatedWords[selectedLanguage] = [
...(updatedWords[selectedLanguage] || []),
...newWords
];
return updatedWords;
});
reversalLettersStore.update((letters) => {
const updatedLetters = { ...letters };
updatedLetters[selectedLanguage] = [
...(updatedLetters[selectedLanguage] || []),
letter
];
return updatedLetters;
});
}

Comment on lines 200 to 239
async function checkIfScrolledToBottom(event) {
if (isFetching) return;

if (
(selectedLanguage === reversalLanguage && reversalWordsList.length > 0) ||
(selectedLanguage === vernacularLanguage && vernacularWordsList.length > 0)
) {
let div = event.target;
const threshold = 100;

if (div.scrollHeight - div.scrollTop - div.clientHeight < threshold) {
const currentIndex = currentAlphabet.indexOf(selectedLetter);
if (!loadedReversalLetters.has(currentAlphabet[currentIndex + 1])) {
if (currentIndex < currentAlphabet.length - 1) {
isFetching = true;
await fetchWords(currentAlphabet[currentIndex + 1]);
isFetching = false;
}
}
} else if (
(selectedLanguage === reversalLanguage &&
loadedReversalLetters.has(selectedLetter)) ||
selectedLanguage === vernacularLanguage
) {
const allLetters = div.querySelectorAll('[id^="letter-"]');
let visibleLetter = null;

allLetters.forEach((letterDiv) => {
const rect = letterDiv.getBoundingClientRect();
if (rect.top >= 0 && rect.bottom <= window.innerHeight) {
visibleLetter = letterDiv.id.split('-')[1];
}
});

if (visibleLetter) {
selectedLetter = visibleLetter;
}
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Simplify scroll handling logic and add bounds checking

The checkIfScrolledToBottom function is overly complex and doesn't properly check array bounds when accessing the next letter.

 async function checkIfScrolledToBottom(event) {
     if (isFetching) return;

     if (
         (selectedLanguage === reversalLanguage && reversalWordsList.length > 0) ||
         (selectedLanguage === vernacularLanguage && vernacularWordsList.length > 0)
     ) {
         let div = event.target;
         const threshold = 100;

         if (div.scrollHeight - div.scrollTop - div.clientHeight < threshold) {
             const currentIndex = currentAlphabet.indexOf(selectedLetter);
-            if (!loadedReversalLetters.has(currentAlphabet[currentIndex + 1])) {
-                if (currentIndex < currentAlphabet.length - 1) {
+            if (currentIndex < currentAlphabet.length - 1) {
+                const nextLetter = currentAlphabet[currentIndex + 1];
+                if (!loadedReversalLetters.has(nextLetter)) {
                     isFetching = true;
-                    await fetchWords(currentAlphabet[currentIndex + 1]);
+                    await fetchWords(nextLetter);
                     isFetching = false;
                 }
             }
         } else if (
             (selectedLanguage === reversalLanguage &&
                 loadedReversalLetters.has(selectedLetter)) ||
             selectedLanguage === vernacularLanguage
         ) {
             const allLetters = div.querySelectorAll('[id^="letter-"]');
             let visibleLetter = null;

+            // Find the first letter that is at least partially visible
             allLetters.forEach((letterDiv) => {
                 const rect = letterDiv.getBoundingClientRect();
-                if (rect.top >= 0 && rect.bottom <= window.innerHeight) {
+                if (rect.top <= window.innerHeight && rect.bottom >= 0) {
                     visibleLetter = letterDiv.id.split('-')[1];
                 }
             });

             if (visibleLetter) {
                 selectedLetter = visibleLetter;
             }
         }
     }
 }
📝 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.

Suggested change
async function checkIfScrolledToBottom(event) {
if (isFetching) return;
if (
(selectedLanguage === reversalLanguage && reversalWordsList.length > 0) ||
(selectedLanguage === vernacularLanguage && vernacularWordsList.length > 0)
) {
let div = event.target;
const threshold = 100;
if (div.scrollHeight - div.scrollTop - div.clientHeight < threshold) {
const currentIndex = currentAlphabet.indexOf(selectedLetter);
if (!loadedReversalLetters.has(currentAlphabet[currentIndex + 1])) {
if (currentIndex < currentAlphabet.length - 1) {
isFetching = true;
await fetchWords(currentAlphabet[currentIndex + 1]);
isFetching = false;
}
}
} else if (
(selectedLanguage === reversalLanguage &&
loadedReversalLetters.has(selectedLetter)) ||
selectedLanguage === vernacularLanguage
) {
const allLetters = div.querySelectorAll('[id^="letter-"]');
let visibleLetter = null;
allLetters.forEach((letterDiv) => {
const rect = letterDiv.getBoundingClientRect();
if (rect.top >= 0 && rect.bottom <= window.innerHeight) {
visibleLetter = letterDiv.id.split('-')[1];
}
});
if (visibleLetter) {
selectedLetter = visibleLetter;
}
}
}
}
async function checkIfScrolledToBottom(event) {
if (isFetching) return;
if (
(selectedLanguage === reversalLanguage && reversalWordsList.length > 0) ||
(selectedLanguage === vernacularLanguage && vernacularWordsList.length > 0)
) {
let div = event.target;
const threshold = 100;
if (div.scrollHeight - div.scrollTop - div.clientHeight < threshold) {
const currentIndex = currentAlphabet.indexOf(selectedLetter);
if (currentIndex < currentAlphabet.length - 1) {
const nextLetter = currentAlphabet[currentIndex + 1];
if (!loadedReversalLetters.has(nextLetter)) {
isFetching = true;
await fetchWords(nextLetter);
isFetching = false;
}
}
} else if (
(selectedLanguage === reversalLanguage &&
loadedReversalLetters.has(selectedLetter)) ||
selectedLanguage === vernacularLanguage
) {
const allLetters = div.querySelectorAll('[id^="letter-"]');
let visibleLetter = null;
// Find the first letter that is at least partially visible
allLetters.forEach((letterDiv) => {
const rect = letterDiv.getBoundingClientRect();
if (rect.top <= window.innerHeight && rect.bottom >= 0) {
visibleLetter = letterDiv.id.split('-')[1];
}
});
if (visibleLetter) {
selectedLetter = visibleLetter;
}
}
}
}

Load Data from SQL for search

Issue #863

work in progress for dab search

Issue #683

Added search button

Search UI changes, Hamburger menu, Back navigation

Added Search sub-bar

Removed hardcoded color for search

Issue #683

Added special characters, whole words, accents and tones options to DAB search

First full implementation of Search

Continued work on search

Finished select word support for search

Freeze header on search scroll

Fix search icon misalign

Freeze word navigation strip

Added support for multi-word searching. Greatly improved efficiency

Formatting
let scrollContainer;
let wordIds;
let selectedLanguage;

// Subscribe to stores
currentReversalLettersStore.subscribe((value) => (loadedReversalLetters = new Set(value)));
Copy link
Member

@chrisvire chrisvire Apr 24, 2025

Choose a reason for hiding this comment

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

I didn't notice this code from the previous PRs. This is not the correct way to use stores. You shouldn't be directly subscribing to them from a .svelte file. You should be using the $store syntax which does auto subscription/unsubscription.

{#if selectedWord}
{#if !showBackButton}
{(showBackButton = true)}
{/if}
Copy link
Member

Choose a reason for hiding this comment

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

I am not quite sure what you are trying to achieve with showBackButton, but this is not the way to do it.

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: 0

♻️ Duplicate comments (7)
src/routes/lexicon/+page.svelte (7)

186-196: 🛠️ Refactor suggestion

Fix non-strict comparison and use Svelte binding for DOM access

The switchLanguage function has two issues:

  1. It uses non-strict comparison (!=) instead of strict comparison (!==)
  2. It directly accesses the DOM with querySelector instead of using the Svelte binding

Apply these changes:

 function switchLanguage(language) {
     selectedLanguageStore.set(language);
     selectedLetter = currentAlphabet[0];
-    if (selectedLanguage != vernacularLanguage) {
+    if (selectedLanguage !== vernacularLanguage) {
         fetchWords();
     }
-    const scrollableDiv = document.querySelector('.flex-1.overflow-y-auto.bg-base-100');
-    if (scrollableDiv) {
-        scrollableDiv.scrollTop = 0;
-    }
+    if (scrollContainer) {
+        scrollContainer.scrollTop = 0;
+    }
 }

90-156: 🛠️ Refactor suggestion

Improve error handling in loadLetterData function

The function lacks proper error handling for failed responses and contains console.log statements for debugging.

Add try-catch blocks and improve error handling:

 async function loadLetterData(letter) {
     let newWords = [];

     const index = reversalIndexes[defaultReversalKey];
     const files = index[letter] || [];
     for (const file of files) {
         const reversalFile = `${base}/reversal/${defaultReversalKey}/${file}`;
+        try {
             const response = await fetch(reversalFile);
             if (response.ok) {
                 const data = await response.json();
                 const currentFileWords = Object.entries(data).map(([word, entries]) => {
                     return {
                         word: word,
                         indexes: entries.map((entry) => entry.index),
                         vernacularWords: entries
                             .map((entry) => {
                                 const foundWord = vernacularWordsList.find(
                                     (vw) => vw.id === entry.index
                                 );
                                 if (foundWord) {
                                     return {
                                         name: foundWord.name,
                                         homonymIndex: foundWord.homonym_index || 0
                                     };
                                 } else {
-                                    console.log(
-                                        `Index ${entry.index} not found in vernacularWordsList`
-                                    );
+                                    // Silently ignore missing indices or log to a dedicated logging system
                                     return null; // Return null for missing indexes
                                 }
                             })
                             .filter((index) => index !== null), // Filter out null values
                         letter: letter
                     };
                 });

                 currentFileWords.forEach((newWord) => {
                     const existingWord = newWords.find((w) => w.word === newWord.word);
                     if (existingWord) {
                         existingWord.indexes = [
                             ...new Set([...existingWord.indexes, ...newWord.indexes])
                         ];
                     } else {
                         newWords.push(newWord);
                     }
                 });
+            } else {
+                console.error(`Failed to fetch file: ${reversalFile}. Status: ${response.status}`);
+            }
+        } catch (error) {
+            console.error(`Error loading data for letter ${letter} from ${file}:`, error);
+        }
     }

     // Update stores with the new data
     // ...rest of function remains unchanged

58-88: 🛠️ Refactor suggestion

Add error handling to fetchWords function

The fetchWords function lacks error handling for potential failures during letter data loading.

Add a try-catch block:

 async function fetchWords(letter = selectedLetter) {
     if (selectedLanguage === reversalLanguage && !loadedReversalLetters.has(letter)) {
         console.log('Loading letter data:', letter);

         const letterIndex = alphabets.reversal.indexOf(letter);
         const lettersToLoad = alphabets.reversal
             .slice(0, letterIndex)
             .filter((l) => !loadedReversalLetters.has(l));

         // Load all required letters in parallel
+        try {
             await Promise.all(lettersToLoad.map(loadLetterData));
 
             // Load the current letter
             await loadLetterData(letter);
 
             // Sort the results based on the selectedLanguage's alphabet
             reversalWordsStore.update((words) => {
                 const updatedWords = { ...words };
                 updatedWords[selectedLanguage] = (updatedWords[selectedLanguage] || []).sort(
                     (a, b) => {
                         const alphabet = currentAlphabet;
                         return (
                             alphabet.indexOf(a.word[0].toLowerCase()) -
                             alphabet.indexOf(b.word[0].toLowerCase())
                         );
                     }
                 );
                 return updatedWords;
             });
+        } catch (error) {
+            console.error('Error loading letter data:', error);
+            // Consider showing a user-friendly error message
+        }
     }
 }

200-239: Simplify scroll handling logic and add bounds checking

The checkIfScrolledToBottom function has overly complex logic and doesn't properly check array bounds.

Simplify the logic:

 async function checkIfScrolledToBottom(event) {
     if (isFetching) return;

     if (
         (selectedLanguage === reversalLanguage && reversalWordsList.length > 0) ||
         (selectedLanguage === vernacularLanguage && vernacularWordsList.length > 0)
     ) {
         let div = event.target;
         const threshold = 100;

         if (div.scrollHeight - div.scrollTop - div.clientHeight < threshold) {
             const currentIndex = currentAlphabet.indexOf(selectedLetter);
-            if (!loadedReversalLetters.has(currentAlphabet[currentIndex + 1])) {
-                if (currentIndex < currentAlphabet.length - 1) {
+            if (currentIndex < currentAlphabet.length - 1) {
+                const nextLetter = currentAlphabet[currentIndex + 1];
+                if (!loadedReversalLetters.has(nextLetter)) {
                     isFetching = true;
-                    await fetchWords(currentAlphabet[currentIndex + 1]);
+                    await fetchWords(nextLetter);
                     isFetching = false;
                 }
             }
         } else if (
             (selectedLanguage === reversalLanguage &&
                 loadedReversalLetters.has(selectedLetter)) ||
             selectedLanguage === vernacularLanguage
         ) {
             const allLetters = div.querySelectorAll('[id^="letter-"]');
             let visibleLetter = null;

+            // Find the first letter that is at least partially visible
             allLetters.forEach((letterDiv) => {
                 const rect = letterDiv.getBoundingClientRect();
-                if (rect.top >= 0 && rect.bottom <= window.innerHeight) {
+                if (rect.top <= window.innerHeight && rect.bottom >= 0) {
                     visibleLetter = letterDiv.id.split('-')[1];
                 }
             });

             if (visibleLetter) {
                 selectedLetter = visibleLetter;
             }
         }
     }
 }

46-54: 🛠️ Refactor suggestion

Use $store syntax instead of direct subscriptions

Direct subscription to stores in Svelte components is not the recommended pattern. This approach has several drawbacks:

  • Manual subscription management leading to potential memory leaks
  • More verbose code than necessary
  • Doesn't leverage Svelte's built-in reactivity system

Replace the direct subscriptions with the $store syntax:

-// Subscribe to stores
-currentReversalLettersStore.subscribe((value) => (loadedReversalLetters = new Set(value)));
-currentReversalWordsStore.subscribe((value) => (reversalWordsList = value));
-vernacularLanguageStore.subscribe((value) => (vernacularLanguage = value));
-vernacularWordsStore.subscribe((value) => (vernacularWordsList = value));
-
-selectedLanguageStore.subscribe((value) => {
-    selectedLanguage = value;
-});
-selectedLanguageStore.set(vernacularLanguage);
+// Use Svelte's auto-subscription syntax
+$: loadedReversalLetters = new Set($currentReversalLettersStore);
+$: reversalWordsList = $currentReversalWordsStore;
+$: vernacularLanguage = $vernacularLanguageStore;
+$: vernacularWordsList = $vernacularWordsStore;
+$: selectedLanguage = $selectedLanguageStore;
+
+// Initialize selectedLanguageStore
+onMount(() => {
+    selectedLanguageStore.set(vernacularLanguage);
+});

158-161: ⚠️ Potential issue

Fix potential null reference in selectWord function

The selectWord function doesn't properly handle the case where selectedWord could be null after toggling.

Fix the potential null reference by adding a null check:

 function selectWord(word) {
     selectedWord = selectedWord && selectedWord.word === word ? null : word;
-    wordIds = selectedWord.indexes ? selectedWord.indexes : [selectedWord.index];
+    wordIds = selectedWord ? (selectedWord.indexes ? selectedWord.indexes : [selectedWord.index]) : null;
 }

297-301: 🛠️ Refactor suggestion

Remove assignment in conditional template

Using an assignment inside a conditional template is a bad practice and can lead to maintenance issues.

Remove the assignment from the template:

 {#if selectedWord}
-    {#if !showBackButton}
-        {(showBackButton = true)}
-    {/if}
     <WordNavigationStrip currentWord={selectedWord} onSelectWord={selectWord} />

Instead, consider updating the reactive declaration for showBackButton to ensure it's correctly derived from selectedWord:

-$: showBackButton = selectedWord ? true : false;
+$: showBackButton = !!selectedWord;
🧹 Nitpick comments (1)
src/routes/lexicon/+page.svelte (1)

35-35: Simplify reactive declaration for showBackButton

The current declaration uses a redundant ternary operator.

Simplify the reactive declaration:

-$: showBackButton = selectedWord ? true : false;
+$: showBackButton = !!selectedWord;
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 44801f0 and 72a1027.

📒 Files selected for processing (2)
  • src/lib/components/LexiconXMLView.svelte (1 hunks)
  • src/routes/lexicon/+page.svelte (7 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/lib/components/LexiconXMLView.svelte
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: test
🔇 Additional comments (2)
src/routes/lexicon/+page.svelte (2)

260-283: Improve the search button implementation

The search button implementation is well-structured but could benefit from some minor improvements.

The search button is a good addition that enhances the user experience by providing easy access to the search functionality. The code is generally well-written, using the new getRoute function for navigation and properly resetting wordIds before navigation.


285-328: Approve the conditional rendering of lexicon views

The conditional rendering of lexicon views based on the selected word and language is well-implemented.

The code effectively uses conditional rendering to display different views based on the user's selection. The structure is clean and follows good Svelte practices by using consistent binding and event handling across all conditions.

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: 0

🔭 Outside diff range comments (1)
src/routes/lexicon/+page.svelte (1)

245-245: 🛠️ Refactor suggestion

Fix non-strict comparison in onMount function

Similar to the issue in switchLanguage, this function is using a non-strict comparison.

-if (selectedLetter && selectedLanguage != vernacularLanguage) {
+if (selectedLetter && selectedLanguage !== vernacularLanguage) {
     fetchWords();
 }
♻️ Duplicate comments (8)
src/routes/lexicon/+page.svelte (8)

186-196: 🛠️ Refactor suggestion

Fix non-strict comparison and use Svelte binding for DOM access

The function is using a non-strict comparison and directly accessing the DOM with querySelector instead of using Svelte bindings.

 function switchLanguage(language) {
     selectedLanguageStore.set(language);
     selectedLetter = currentAlphabet[0];
-    if (selectedLanguage != vernacularLanguage) {
+    if (selectedLanguage !== vernacularLanguage) {
         fetchWords();
     }
-    const scrollableDiv = document.querySelector('.flex-1.overflow-y-auto.bg-base-100');
-    if (scrollableDiv) {
-        scrollableDiv.scrollTop = 0;
-    }
+    if (scrollContainer) {
+        scrollContainer.scrollTop = 0;
+    }
 }

58-88: 🛠️ Refactor suggestion

Add error handling to fetchWords function

The fetchWords function lacks error handling for potential failures during letter data loading.

 async function fetchWords(letter = selectedLetter) {
     if (selectedLanguage === reversalLanguage && !loadedReversalLetters.has(letter)) {
         console.log('Loading letter data:', letter);

         const letterIndex = alphabets.reversal.indexOf(letter);
         const lettersToLoad = alphabets.reversal
             .slice(0, letterIndex)
             .filter((l) => !loadedReversalLetters.has(l));

         // Load all required letters in parallel
+        try {
             await Promise.all(lettersToLoad.map(loadLetterData));
 
             // Load the current letter
             await loadLetterData(letter);
 
             // Sort the results based on the selectedLanguage's alphabet
             reversalWordsStore.update((words) => {
                 const updatedWords = { ...words };
                 updatedWords[selectedLanguage] = (updatedWords[selectedLanguage] || []).sort(
                     (a, b) => {
                         const alphabet = currentAlphabet;
                         return (
                             alphabet.indexOf(a.word[0].toLowerCase()) -
                             alphabet.indexOf(b.word[0].toLowerCase())
                         );
                     }
                 );
                 return updatedWords;
             });
+        } catch (error) {
+            console.error('Error loading letter data:', error);
+            isFetching = false; // Make sure to reset the flag in case of error
+            // Consider showing a user-friendly error message
+        }
     }
 }

90-156: 🛠️ Refactor suggestion

Improve error handling in loadLetterData function

The loadLetterData function lacks proper error handling for the fetch operation and contains debugging console logs.

 async function loadLetterData(letter) {
     let newWords = [];

     const index = reversalIndexes[defaultReversalKey];
     const files = index[letter] || [];
     for (const file of files) {
         const reversalFile = `${base}/reversal/${defaultReversalKey}/${file}`;
+        try {
             const response = await fetch(reversalFile);
             if (response.ok) {
                 const data = await response.json();
                 const currentFileWords = Object.entries(data).map(([word, entries]) => {
                     return {
                         word: word,
                         indexes: entries.map((entry) => entry.index),
                         vernacularWords: entries
                             .map((entry) => {
                                 const foundWord = vernacularWordsList.find(
                                     (vw) => vw.id === entry.index
                                 );
                                 if (foundWord) {
                                     return {
                                         name: foundWord.name,
                                         homonymIndex: foundWord.homonym_index || 0
                                     };
                                 } else {
-                                    console.log(
-                                        `Index ${entry.index} not found in vernacularWordsList`
-                                    );
+                                    // Silently ignore missing indices or log to a dedicated logging system
                                     return null; // Return null for missing indexes
                                 }
                             })
                             .filter((index) => index !== null), // Filter out null values
                         letter: letter
                     };
                 });

                 currentFileWords.forEach((newWord) => {
                     const existingWord = newWords.find((w) => w.word === newWord.word);
                     if (existingWord) {
                         existingWord.indexes = [
                             ...new Set([...existingWord.indexes, ...newWord.indexes])
                         ];
                     } else {
                         newWords.push(newWord);
                     }
                 });
+            } else {
+                console.error(`Failed to fetch file: ${reversalFile}. Status: ${response.status}`);
+            }
+        } catch (error) {
+            console.error(`Error loading data for letter ${letter} from ${file}:`, error);
+        }
     }

     // Continue with store updates...

200-239: 🛠️ Refactor suggestion

Simplify scroll handling logic and add bounds checking

The checkIfScrolledToBottom function has issues with scroll logic and doesn't properly check array bounds when accessing the next letter.

 async function checkIfScrolledToBottom(event) {
     if (isFetching) return;

     if (
         (selectedLanguage === reversalLanguage && reversalWordsList.length > 0) ||
         (selectedLanguage === vernacularLanguage && vernacularWordsList.length > 0)
     ) {
         let div = event.target;
         const threshold = 100;

         if (div.scrollHeight - div.scrollTop - div.clientHeight < threshold) {
             const currentIndex = currentAlphabet.indexOf(selectedLetter);
-            if (!loadedReversalLetters.has(currentAlphabet[currentIndex + 1])) {
-                if (currentIndex < currentAlphabet.length - 1) {
+            if (currentIndex < currentAlphabet.length - 1) {
+                const nextLetter = currentAlphabet[currentIndex + 1];
+                if (!loadedReversalLetters.has(nextLetter)) {
                     isFetching = true;
-                    await fetchWords(currentAlphabet[currentIndex + 1]);
+                    await fetchWords(nextLetter);
                     isFetching = false;
                 }
             }
         } else if (
             (selectedLanguage === reversalLanguage &&
                 loadedReversalLetters.has(selectedLetter)) ||
             selectedLanguage === vernacularLanguage
         ) {
             const allLetters = div.querySelectorAll('[id^="letter-"]');
             let visibleLetter = null;

+            // Find the first letter that is at least partially visible
             allLetters.forEach((letterDiv) => {
                 const rect = letterDiv.getBoundingClientRect();
-                if (rect.top >= 0 && rect.bottom <= window.innerHeight) {
+                if (rect.top <= window.innerHeight && rect.bottom >= 0) {
                     visibleLetter = letterDiv.id.split('-')[1];
                 }
             });

             if (visibleLetter) {
                 selectedLetter = visibleLetter;
             }
         }
     }
 }

51-54: ⚠️ Potential issue

Store subscription should use Svelte's reactive syntax

Direct store subscription in Svelte components can lead to memory leaks as these subscriptions aren't automatically cleaned up. Use Svelte's reactive $store syntax instead, which handles subscriptions and unsubscriptions automatically.

-selectedLanguageStore.subscribe((value) => {
-    selectedLanguage = value;
-});
-selectedLanguageStore.set(vernacularLanguage);
+$: selectedLanguage = $selectedLanguageStore;
+
+onMount(() => {
+    selectedLanguageStore.set(vernacularLanguage);
+});

46-49: ⚠️ Potential issue

Replace direct store subscriptions with Svelte's reactive syntax

Similar to the issue above, all store subscriptions should use Svelte's reactive $store syntax instead of direct subscriptions.

-// Subscribe to stores
-currentReversalLettersStore.subscribe((value) => (loadedReversalLetters = new Set(value)));
-currentReversalWordsStore.subscribe((value) => (reversalWordsList = value));
-vernacularLanguageStore.subscribe((value) => (vernacularLanguage = value));
-vernacularWordsStore.subscribe((value) => (vernacularWordsList = value));
+// Using Svelte's reactive store syntax
+$: loadedReversalLetters = new Set($currentReversalLettersStore);
+$: reversalWordsList = $currentReversalWordsStore;
+$: vernacularLanguage = $vernacularLanguageStore;
+$: vernacularWordsList = $vernacularWordsStore;

158-161: ⚠️ Potential issue

Fix potential null reference in selectWord function

The selectWord function doesn't properly handle the case where selectedWord could be null after toggling.

 function selectWord(word) {
     selectedWord = selectedWord && selectedWord.word === word ? null : word;
-    wordIds = selectedWord.indexes ? selectedWord.indexes : [selectedWord.index];
+    wordIds = selectedWord ? (selectedWord.indexes ? selectedWord.indexes : [selectedWord.index]) : null;
 }

297-301: ⚠️ Potential issue

Remove assignment in conditional template

Using an assignment inside a conditional template is a bad practice and can lead to maintenance issues.

 {#if selectedWord}
-    {#if !showBackButton}
-        {(showBackButton = true)}
-    {/if}
     <WordNavigationStrip currentWord={selectedWord} onSelectWord={selectWord} />

Since you're already setting showBackButton reactively on line 35, this assignment is unnecessary.

🧹 Nitpick comments (1)
src/routes/lexicon/+page.svelte (1)

35-35: Simplify reactive declaration

This reactive declaration can be simplified.

-$: showBackButton = selectedWord ? true : false;
+$: showBackButton = !!selectedWord;
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 72a1027 and 54be2e0.

📒 Files selected for processing (4)
  • src/lib/components/LexiconReversalListView.svelte (1 hunks)
  • src/lib/components/LexiconVernacularListView.svelte (1 hunks)
  • src/lib/components/LexiconXMLView.svelte (1 hunks)
  • src/routes/lexicon/+page.svelte (7 hunks)
✅ Files skipped from review due to trivial changes (2)
  • src/lib/components/LexiconReversalListView.svelte
  • src/lib/components/LexiconVernacularListView.svelte
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/lib/components/LexiconXMLView.svelte
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: lint
  • GitHub Check: test
🔇 Additional comments (2)
src/routes/lexicon/+page.svelte (2)

269-281: The search button implementation looks good!

The addition of the search button with proper navigation to the search page is well-implemented. It correctly handles setting wordIds to null before navigating, which prevents state issues.


254-259: Good use of conditional classes for layout

The implementation of conditional grid rows based on whether a word is selected is a good practice that improves layout management.

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: 0

♻️ Duplicate comments (7)
src/routes/lexicon/+page.svelte (7)

58-88: Add error handling to fetchWords function

The function lacks error handling for potential failures during letter data loading. If any operation fails, it would throw an unhandled exception.

 async function fetchWords(letter = selectedLetter) {
     if (selectedLanguage === reversalLanguage && !loadedReversalLetters.has(letter)) {
         console.log('Loading letter data:', letter);

         const letterIndex = alphabets.reversal.indexOf(letter);
         const lettersToLoad = alphabets.reversal
             .slice(0, letterIndex)
             .filter((l) => !loadedReversalLetters.has(l));

         // Load all required letters in parallel
+        try {
             await Promise.all(lettersToLoad.map(loadLetterData));

             // Load the current letter
             await loadLetterData(letter);

             // Sort the results based on the selectedLanguage's alphabet
             reversalWordsStore.update((words) => {
                 const updatedWords = { ...words };
                 updatedWords[selectedLanguage] = (updatedWords[selectedLanguage] || []).sort(
                     (a, b) => {
                         const alphabet = currentAlphabet;
                         return (
                             alphabet.indexOf(a.word[0].toLowerCase()) -
                             alphabet.indexOf(b.word[0].toLowerCase())
                         );
                     }
                 );
                 return updatedWords;
             });
+        } catch (error) {
+            console.error('Error loading letter data:', error);
+            isFetching = false; // Ensure we reset fetching state on error
+        }
     }
 }

90-156: Improve error handling and remove console.log in loadLetterData

The function lacks proper error handling for failed responses or potential issues with network requests and JSON parsing.

 async function loadLetterData(letter) {
     let newWords = [];

     const index = reversalIndexes[defaultReversalKey];
     const files = index[letter] || [];
     for (const file of files) {
         const reversalFile = `${base}/reversal/${defaultReversalKey}/${file}`;
+        try {
             const response = await fetch(reversalFile);
             if (response.ok) {
                 const data = await response.json();
                 const currentFileWords = Object.entries(data).map(([word, entries]) => {
                     return {
                         word: word,
                         indexes: entries.map((entry) => entry.index),
                         vernacularWords: entries
                             .map((entry) => {
                                 const foundWord = vernacularWordsList.find(
                                     (vw) => vw.id === entry.index
                                 );
                                 if (foundWord) {
                                     return {
                                         name: foundWord.name,
                                         homonymIndex: foundWord.homonym_index || 0
                                     };
                                 } else {
-                                    console.log(
-                                        `Index ${entry.index} not found in vernacularWordsList`
-                                    );
+                                    // Silently ignore missing indices or log to a dedicated logging system
                                     return null; // Return null for missing indexes
                                 }
                             })
                             .filter((index) => index !== null), // Filter out null values
                         letter: letter
                     };
                 });

                 currentFileWords.forEach((newWord) => {
                     const existingWord = newWords.find((w) => w.word === newWord.word);
                     if (existingWord) {
                         existingWord.indexes = [
                             ...new Set([...existingWord.indexes, ...newWord.indexes])
                         ];
                     } else {
                         newWords.push(newWord);
                     }
                 });
+            } else {
+                console.error(`Failed to fetch file: ${reversalFile}. Status: ${response.status}`);
+            }
+        } catch (error) {
+            console.error(`Error loading data for letter ${letter} from ${file}:`, error);
+        }
     }

     reversalWordsStore.update((words) => {
         const updatedWords = { ...words };
         updatedWords[selectedLanguage] = [
             ...(updatedWords[selectedLanguage] || []),
             ...newWords
         ];
         return updatedWords;
     });

     reversalLettersStore.update((letters) => {
         const updatedLetters = { ...letters };
         updatedLetters[selectedLanguage] = [
             ...(updatedLetters[selectedLanguage] || []),
             letter
         ];
         return updatedLetters;
     });
 }

158-161: Fix potential null reference in selectWord function

The selectWord function doesn't properly handle the case where selectedWord could be null after toggling.

 function selectWord(word) {
     selectedWord = selectedWord && selectedWord.word === word ? null : word;
-    wordIds = selectedWord.indexes ? selectedWord.indexes : [selectedWord.index];
+    wordIds = selectedWord ? (selectedWord.indexes ? selectedWord.indexes : [selectedWord.index]) : null;
 }

186-196: Fix non-strict comparison and use Svelte binding for DOM access

The function is using a non-strict comparison and directly accessing the DOM with querySelector instead of using Svelte bindings.

 function switchLanguage(language) {
     selectedLanguageStore.set(language);
     selectedLetter = currentAlphabet[0];
-    if (selectedLanguage != vernacularLanguage) {
+    if (selectedLanguage !== vernacularLanguage) {
         fetchWords();
     }
-    const scrollableDiv = document.querySelector('.flex-1.overflow-y-auto.bg-base-100');
-    if (scrollableDiv) {
-        scrollableDiv.scrollTop = 0;
-    }
+    if (scrollContainer) {
+        scrollContainer.scrollTop = 0;
+    }
 }

200-239: Simplify scroll handling logic and add bounds checking

The checkIfScrolledToBottom function is overly complex and doesn't properly check array bounds when accessing the next letter.

 async function checkIfScrolledToBottom(event) {
     if (isFetching) return;

     if (
         (selectedLanguage === reversalLanguage && reversalWordsList.length > 0) ||
         (selectedLanguage === vernacularLanguage && vernacularWordsList.length > 0)
     ) {
         let div = event.target;
         const threshold = 100;

         if (div.scrollHeight - div.scrollTop - div.clientHeight < threshold) {
             const currentIndex = currentAlphabet.indexOf(selectedLetter);
-            if (!loadedReversalLetters.has(currentAlphabet[currentIndex + 1])) {
-                if (currentIndex < currentAlphabet.length - 1) {
+            if (currentIndex < currentAlphabet.length - 1) {
+                const nextLetter = currentAlphabet[currentIndex + 1];
+                if (!loadedReversalLetters.has(nextLetter)) {
                     isFetching = true;
-                    await fetchWords(currentAlphabet[currentIndex + 1]);
+                    await fetchWords(nextLetter);
                     isFetching = false;
                 }
             }
         } else if (
             (selectedLanguage === reversalLanguage &&
                 loadedReversalLetters.has(selectedLetter)) ||
             selectedLanguage === vernacularLanguage
         ) {
             const allLetters = div.querySelectorAll('[id^="letter-"]');
             let visibleLetter = null;

+            // Find the first letter that is at least partially visible
             allLetters.forEach((letterDiv) => {
                 const rect = letterDiv.getBoundingClientRect();
-                if (rect.top >= 0 && rect.bottom <= window.innerHeight) {
+                if (rect.top <= window.innerHeight && rect.bottom >= 0) {
                     visibleLetter = letterDiv.id.split('-')[1];
                 }
             });

             if (visibleLetter) {
                 selectedLetter = visibleLetter;
             }
         }
     }
 }

297-302: Remove assignment in conditional template

Using an assignment inside a conditional template is a bad practice and can lead to maintenance issues.

 {#if selectedWord}
-    {#if !showBackButton}
-        {(showBackButton = true)}
-    {/if}
     <WordNavigationStrip currentWord={selectedWord} onSelectWord={selectWord} />

46-54: 🛠️ Refactor suggestion

Replace manual store subscriptions with Svelte's auto-subscription syntax

Manual store subscriptions should be avoided in Svelte components as they don't automatically unsubscribe when the component is destroyed, potentially causing memory leaks. Use Svelte's built-in $store syntax instead.

-// Subscribe to stores
-currentReversalLettersStore.subscribe((value) => (loadedReversalLetters = new Set(value)));
-currentReversalWordsStore.subscribe((value) => (reversalWordsList = value));
-vernacularLanguageStore.subscribe((value) => (vernacularLanguage = value));
-vernacularWordsStore.subscribe((value) => (vernacularWordsList = value));
-
-selectedLanguageStore.subscribe((value) => {
-    selectedLanguage = value;
-});
+$: loadedReversalLetters = new Set($currentReversalLettersStore);
+$: reversalWordsList = $currentReversalWordsStore;
+$: vernacularLanguage = $vernacularLanguageStore;
+$: vernacularWordsList = $vernacularWordsStore;
+$: selectedLanguage = $selectedLanguageStore;
🧹 Nitpick comments (3)
src/routes/lexicon/+page.svelte (3)

35-35: Convert direct reactive assignment to proper Svelte reactive declaration

This line creates a reactive declaration but uses assignment syntax. It's more idiomatic to use Svelte's reactive declaration syntax.

-$: showBackButton = selectedWord ? true : false;
+$: showBackButton = !!selectedWord;

245-251: Add non-strict equality check in onMount

Similar to the issue in switchLanguage, you're using non-strict equality comparison here.

 onMount(() => {
-    if (selectedLetter && selectedLanguage != vernacularLanguage) {
+    if (selectedLetter && selectedLanguage !== vernacularLanguage) {
         fetchWords();
     }
     if (config.programType !== 'DAB') {
         goto(`${base}/text`);
     }
 });

302-310: Consider simplifying conditional styling

The container styling is repeated in three different places with almost identical code. Consider extracting this to a common variable or component to reduce duplication.

+<script>
+    // At the top of your script section
+    $: containerClasses = "flex-1 overflow-y-auto bg-base-100 width-full";
+    $: containerStyles = selectedWord 
+        ? "background-color: var(--PrimaryColor);" 
+        : "";
+</script>

 {#if selectedWord}
     <WordNavigationStrip currentWord={selectedWord} onSelectWord={selectWord} />
     <div
         id="container"
-        class="flex-1 overflow-y-auto bg-base-100 width-full"
-        style="background-color: var(--PrimaryColor);"
+        class={containerClasses}
+        style={containerStyles}
         bind:this={scrollContainer}
         on:scroll={checkIfScrolledToBottom}
     >
         <LexiconXmlView {wordIds} onSelectWord={selectWord} />
     </div>

And apply similar changes to the other container divs.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 54be2e0 and ce111cb.

📒 Files selected for processing (4)
  • src/lib/components/LexiconReversalListView.svelte (1 hunks)
  • src/lib/components/LexiconVernacularListView.svelte (1 hunks)
  • src/lib/components/LexiconXMLView.svelte (1 hunks)
  • src/routes/lexicon/+page.svelte (7 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/lib/components/LexiconReversalListView.svelte
  • src/lib/components/LexiconVernacularListView.svelte
  • src/lib/components/LexiconXMLView.svelte
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: test

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: 0

🧹 Nitpick comments (1)
src/lib/components/LexiconEntryView.svelte (1)

110-121: Consider extracting styling logic for improved maintainability

While the current implementation works well, consider extracting the styling logic into a separate function to improve code readability and maintainability as the component grows.

- // I've added appropriate styling based on class name
- if (className.includes('sensenumber')) {
-     output += ` style="color: var(--TextColor1); font-weight: bold;"`;
- } else if (className.includes('vernacular')) {
-     output += ` style="color: var(--TextColor2);"`;
- } else if (className.includes('example')) {
-     output += ` style="color: var(--TextColor3); font-style: italic;"`;
- } else if (className.includes('definition')) {
-     output += ` style="color: var(--TextColor); font-weight: normal;"`;
- }
+ // Apply appropriate styling based on class name
+ output += getStyleForClassName(className);

// Add this function elsewhere in the script
function getStyleForClassName(className) {
  if (className.includes('sensenumber')) {
    return ` style="color: var(--TextColor1); font-weight: bold;"`;
  } else if (className.includes('vernacular')) {
    return ` style="color: var(--TextColor2);"`;
  } else if (className.includes('example')) {
    return ` style="color: var(--TextColor3); font-style: italic;"`;
  } else if (className.includes('definition')) {
    return ` style="color: var(--TextColor); font-weight: normal;"`;
  }
  return '';
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 921b67a and ffb2733.

📒 Files selected for processing (8)
  • src/lib/components/AlphabetStrip.svelte (1 hunks)
  • src/lib/components/LexiconEntryView.svelte (4 hunks)
  • src/lib/components/LexiconReversalListView.svelte (2 hunks)
  • src/lib/components/LexiconVernacularListView.svelte (2 hunks)
  • src/lib/components/LexiconXMLView.svelte (1 hunks)
  • src/lib/components/SearchForm.svelte (4 hunks)
  • src/lib/components/WordNavigationStrip.svelte (2 hunks)
  • src/routes/lexicon/search/+page.svelte (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (7)
  • src/lib/components/LexiconReversalListView.svelte
  • src/lib/components/AlphabetStrip.svelte
  • src/lib/components/LexiconVernacularListView.svelte
  • src/lib/components/WordNavigationStrip.svelte
  • src/lib/components/SearchForm.svelte
  • src/routes/lexicon/search/+page.svelte
  • src/lib/components/LexiconXMLView.svelte
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: lint
  • GitHub Check: test
🔇 Additional comments (5)
src/lib/components/LexiconEntryView.svelte (5)

64-64: LGTM: Consistent error styling

The error message now uses the theme's CSS variable (--TextColor6) for error text, ensuring consistent styling across the application.


102-102: LGTM: Improved link styling with CSS variables

Replacing anchor tags with styled spans maintains functionality while ensuring consistent appearance through CSS variables. This is a good approach that follows the theming system.


110-121: LGTM: Conditional styling for semantic elements

Good implementation of conditional styling based on class names using CSS variables. This ensures consistent visual hierarchy across different types of lexical content.


155-156: LGTM: Consistent separator styling

Using the --SettingsSeparatorColor variable for horizontal rule elements ensures visual consistency between entries.


201-203: LGTM: Consistent container styling

The pre element now correctly uses theme variables for background and text colors, ensuring the container blends well with the rest of the UI.

MrCars0n and others added 5 commits April 25, 2025 20:41
* Create route for lexicon page

Create component for reversal view

Create reusable alphabet strip component

Implement initial lexicon reversal interface

Add lexicon page data loading

Add DAB program type route handling

Add missing imports to lexicon and main routing pages

lint format for page.js file for lexicon

Create first reversal page draft

Converted page to components

Dynamically load alphabet from dictionary config

Dynamically load alphabet from dictionary config

Lift language state to parent component

Add reversal lanuage name and letter change detection

Lazy load English words and base for XML implementation

Create Language Tabs Component and Parse lang
Issue #685

Moved the code for the language tabs from
LexiconReversalView and created a separate
component. Also, parsed lang from writing systems
and updated index.d.ts with displayLang

Change English wording and fix selectedLetter

Name vernacular language correctly and organize code

Create new index.ts file for lexicon page

Name vernacular language correctly and organize code

Fix language tab naming convention

Removed duplicate code for parsing lang

Issue #685

Removed Unnecessary Code and Lexicon Folder

Issue #685

Removed hardcoding

Switch order of language tabs

* Removed page.ts

* Changed use of "slot" to "render" for navbar

* List of words

* Fix mobile layout

* Fix lexicon auto load

* Fix word id mismatch

* Add margins to alphabet strip

* Add first draft of vernacular letter id handling

* Restore dictionary functionality while preserving styling

* Fixed vernacular letter id handling

* Create XML element and reorganize

* Fix loading of second letter on tab change

* Fix alphabet bar navigation bug

* Store selectedLanguage after XMLView

* Add word subtexts, indexes, and alphabet strip size change

* Add word navigation component

* Add XML new lines

* Add multi-index XML call and new line formatting

* Bug fixes

* Moved vernacular query to page.js, simplified page.svelte

* Revert to threshold

* Fixed lazy loading glitches on firefox

* Fixed sqlite fetch issue

* add single entry style type

* Add hyperlinks to headwords in XML

* Remove unused variable and comments

* show single entry styles

* Added stores. Refactored lexicon code. Included reversal/vernacular mapped words in store to improve efficiency

* Merge and formatting

* Applied suggestions

* Small changes

* Small changes

* miscellaneous small refactors

* Format

* Fix test

* Fix accessibility issues

* Add fallback for summary without matches

* Fix comma issue. Fix duplicate listeners

* Add proper type definition for vernacularWordsStore

* Improve store definitions

* Vernacular word hyperlink fix

* Fix lint issues

* Fix language tab to match native app

* Remove padding

* Applied feedback for language tabs

* fix lint

* Fix spacing

* Adjust word list spacing

* Fix navbar hiding on scroll and div organization

* Fix more div organizaiton

* Spacing fix

* Implement lexicon error handling

* Implement fetch logic for reversal view

* Fix a potential security vulnerability

* fix lint

* Remove hardcoded colors

* Split LexiconReversalListView into separate components.

* Remove hardcoded color

* Remove alphabetStrip artifact

* Add proper reversal file fetch

* Remove phantom endpoint call

* Add grid view

* Remove HEAD request in file loading

* Remove unnecessary comments

* Add grid view

* Add grid view

* Remove redundancy

* convertReversalIndex create index.json

* Use ReversalIndex to load reversal files

* Fix wide screen gap

* Fix display on wide screen

* Rename AlphabetStrip

* Rename LexiconReversalView

* Rename LexiconXMLView

---------

Co-authored-by: EthanFennell <[email protected]>
Co-authored-by: Carson Kramer <[email protected]>
Co-authored-by: AslanRules <[email protected]>
Co-authored-by: Chris Hubbard <[email protected]>
@chrisvire
Copy link
Member

Fixed branch issues in #806

@chrisvire chrisvire closed this May 12, 2025
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.

8 participants