All files / app/pages/shopping-list shopping-lists.ts

37.17% Statements 29/78
63.63% Branches 14/22
35.29% Functions 6/17
35.21% Lines 25/71

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172                                          3x 1x 1x 1x 1x   1x 1x   1x             1x     1x 1x 1x 1x           1x 1x     1x 1x     1x         1x 1x   1x                                                                                                     1x 1x 1x 1x                                                                                                        
import { Component, OnDestroy, OnInit, signal, ChangeDetectorRef } from '@angular/core';
import { Lists } from '../../types';
import { SupabaseConnector } from '../../services/supabase-connector';
import { PowerSyncService, USER_ID_PLACEHOLDER, USER_LIST_ID_PLACEHOLDER } from '../../services/powersync';
import { Router } from '@angular/router';
import { LucideAngularModule, ListChecks, Plus, ArrowRight, Layers } from 'lucide-angular';
import { AsyncPipe } from '@angular/common';
import { take } from 'rxjs';
import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
import { TranslocoModule } from '@jsverse/transloco';
 
type ListWithUserCount = Lists & {
  other_users_count?: number | null;
};
 
@Component({
  selector: 'app-shopping-lists',
  imports: [LucideAngularModule, AsyncPipe, ReactiveFormsModule, TranslocoModule],
  templateUrl: './shopping-lists.html',
  styleUrl: './shopping-lists.scss',
})
export class ShoppingLists implements OnInit, OnDestroy {
  readonly lists = signal<ListWithUserCount[]>([]);
  readonly listsLoaded = signal(false);
  userId: string | null = null;
  readonly icons = { ListChecks, Plus, ArrowRight, Layers };
 
  private stopListsWatch: (() => void) | null = null;
  private watchedListsQuery: { close?: () => void } | null = null;
 
  readonly isOnline = signal<boolean>(navigator.onLine);
  private readonly handleOnlineStatusChange = () => {
    this.isOnline.set(navigator.onLine);
    this.cdr.detectChanges();
  };
 
  // eslint-disable-next-line @typescript-eslint/unbound-method
  protected control = new FormControl('', [Validators.required, Validators.minLength(1)]);
 
  constructor(
    private supabase: SupabaseConnector,
    protected readonly powerSync: PowerSyncService,
    private readonly router: Router,
    private cdr: ChangeDetectorRef
  ) {
  }
 
  // eslint-disable-next-line @typescript-eslint/no-misused-promises
  async ngOnInit() {
    window.addEventListener('online', this.handleOnlineStatusChange);
    window.addEventListener('offline', this.handleOnlineStatusChange);
 
    // eslint-disable-next-line @typescript-eslint/no-misused-promises
    this.powerSync.ready$.subscribe(async initialized => {
      Iif (initialized) {
        await this.initialize();
      }
      take(1);
    });
  }
 
  ngOnDestroy(): void {
    window.removeEventListener('online', this.handleOnlineStatusChange);
    window.removeEventListener('offline', this.handleOnlineStatusChange);
 
    this.disposeListsWatch();
  }
 
  private async initialize(): Promise<void> {
    this.userId = USER_ID_PLACEHOLDER;
    this.getLists();
  }
 
  getLists() {
    this.disposeListsWatch();
    this.listsLoaded.set(false);
    this.lists.set([]);
 
    const sql = `
      SELECT l.*
      FROM "Lists" l
      INNER JOIN "UserLists" ul ON ul.list = l.id
      WHERE ul.user = ?
      ORDER BY l.created_at DESC
    `;
 
    const watched = this.powerSync.query<Lists>(sql, [USER_ID_PLACEHOLDER]).watch();
    this.watchedListsQuery = watched as unknown as { close?: () => void };
 
    this.stopListsWatch = watched.registerListener({
      onData: async (data) => {
        try {
          const rows = data as Lists[];
          const listsWithUserCount: ListWithUserCount[] = await Promise.all(rows.map(async list => ({
            ...list,
            other_users_count: list.id ? await this.supabase.getUserCountForList(list.id)-1 : null
          })));
          this.lists.set(listsWithUserCount);
          this.listsLoaded.set(true);
          console.log('Lists set successfully!', listsWithUserCount.length);
          this.cdr.detectChanges();
        } catch (e) {
          console.error('Error updating lists in onData:', e);
          this.listsLoaded.set(true);
          setTimeout(() => alert(`Error in onData: ${String(e)}`), 100);
        }
      },
      onError: (error) => {
        console.error('Query error:', error);
        this.listsLoaded.set(true);
      }
    });
 
  }
 
  private disposeListsWatch(): void {
    this.stopListsWatch?.();
    this.stopListsWatch = null;
    this.watchedListsQuery?.close?.();
    this.watchedListsQuery = null;
  }
 
  async addList(name: string, description: string = ''): Promise<void> {
    if (!name) return;
    const parts = crypto.getRandomValues(new Uint32Array(2));
    // eslint-disable-next-line no-bitwise
    const randomBigInt: bigint = (BigInt(parts[0]) << 16n) | BigInt(parts[1]);
    void randomBigInt;
 
    await this.powerSync.db.execute(
      `INSERT INTO "Lists" (id, created_at, name, description) VALUES (?, datetime(), ?, ?)`,
      [String(randomBigInt), name, description]
    );
    console.log('List created with ID:', randomBigInt);
 
    await this.powerSync.execute(
      `INSERT INTO "UserLists" (id, created_at, user, list) VALUES (?, datetime(), ?, ?)`,
      [{user: this.userId, list: randomBigInt} as USER_LIST_ID_PLACEHOLDER, this.userId, String(randomBigInt)]
    );
  }
 
  async createList(input: HTMLInputElement): Promise<void> {
    const name = input.value.trim();
    if (!name) { 
      this.control.markAsTouched();
      return; 
    }
 
    try {
      await this.addList(name);
      input.value = '';
      // Force trigger an event just in case it's a zone issue
      window.dispatchEvent(new Event('resize')); 
      console.log('List creation succeeded');
    } catch (e) {
      console.error('List creation failed:', e);
      alert(`Error creating list: ${String(e)}`);
    }
  }
 
  openList(list: Lists): void {
    void this.router.navigate(['/list'], {
      queryParams: { listId: list.id }
    });
  }
 
  async getOtherUsersCount(listId: bigint): Promise<number> {
    const value = await this.supabase.getUserCountForList(listId);
    return value;
  }
}