Browse Source
			
			
			
			
				
		- Deleted ContactScanView.vue and its route from the router. - Renamed ContactQRScanView.vue to ContactQRScanFullView.vue. - Updated all router paths, names, and references for consistency. - Fixed related links and imports to use the new view/component name.
				 5 changed files with 567 additions and 95 deletions
			
			
		@ -0,0 +1,430 @@ | 
				
			|||||
 | 
					<template> | 
				
			||||
 | 
					  <!-- CONTENT --> | 
				
			||||
 | 
					  <section id="Content" class="relativew-[100vw] h-[100vh]"> | 
				
			||||
 | 
					    <div | 
				
			||||
 | 
					      class="absolute inset-x-0 bottom-0 bg-black/50 p-6 pb-[calc(env(safe-area-inset-bottom)+1.5rem)]" | 
				
			||||
 | 
					    > | 
				
			||||
 | 
					      <p class="text-center text-white mb-3"> | 
				
			||||
 | 
					        Point your camera at a TimeSafari contact QR code to scan it | 
				
			||||
 | 
					        automatically. | 
				
			||||
 | 
					      </p> | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					      <p v-if="error" class="text-center text-rose-300 mb-3">{{ error }}</p> | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					      <div class="flex justify-center items-center"> | 
				
			||||
 | 
					        <button | 
				
			||||
 | 
					          class="text-center text-slate-600 leading-none bg-white p-2 rounded-full drop-shadow-lg" | 
				
			||||
 | 
					          @click="handleBack" | 
				
			||||
 | 
					        > | 
				
			||||
 | 
					          <font-awesome icon="xmark" class="size-6"></font-awesome> | 
				
			||||
 | 
					        </button> | 
				
			||||
 | 
					      </div> | 
				
			||||
 | 
					    </div> | 
				
			||||
 | 
					  </section> | 
				
			||||
 | 
					</template> | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					<script lang="ts"> | 
				
			||||
 | 
					import { Component, Vue } from "vue-facing-decorator"; | 
				
			||||
 | 
					import { Router } from "vue-router"; | 
				
			||||
 | 
					import { logger } from "../utils/logger"; | 
				
			||||
 | 
					import { QRScannerFactory } from "../services/QRScanner/QRScannerFactory"; | 
				
			||||
 | 
					import QuickNav from "../components/QuickNav.vue"; | 
				
			||||
 | 
					import { NotificationIface } from "../constants/app"; | 
				
			||||
 | 
					import { db } from "../db/index"; | 
				
			||||
 | 
					import { Contact } from "../db/tables/contacts"; | 
				
			||||
 | 
					import { getContactJwtFromJwtUrl } from "../libs/crypto"; | 
				
			||||
 | 
					import { decodeEndorserJwt } from "../libs/crypto/vc"; | 
				
			||||
 | 
					import { retrieveSettingsForActiveAccount } from "../db/index"; | 
				
			||||
 | 
					import { setVisibilityUtil } from "../libs/endorserServer"; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					interface QRScanResult { | 
				
			||||
 | 
					  rawValue?: string; | 
				
			||||
 | 
					  barcode?: string; | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					@Component({ | 
				
			||||
 | 
					  components: { | 
				
			||||
 | 
					    QuickNav, | 
				
			||||
 | 
					  }, | 
				
			||||
 | 
					}) | 
				
			||||
 | 
					export default class ContactQRScan extends Vue { | 
				
			||||
 | 
					  $notify!: (notification: NotificationIface, timeout?: number) => void; | 
				
			||||
 | 
					  $router!: Router; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					  isScanning = false; | 
				
			||||
 | 
					  error: string | null = null; | 
				
			||||
 | 
					  activeDid = ""; | 
				
			||||
 | 
					  apiServer = ""; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					  // Add new properties to track scanning state | 
				
			||||
 | 
					  private lastScannedValue: string = ""; | 
				
			||||
 | 
					  private lastScanTime: number = 0; | 
				
			||||
 | 
					  private readonly SCAN_DEBOUNCE_MS = 2000; // Prevent duplicate scans within 2 seconds | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					  // Add cleanup tracking | 
				
			||||
 | 
					  private isCleaningUp = false; | 
				
			||||
 | 
					  private isMounted = false; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					  async created() { | 
				
			||||
 | 
					    try { | 
				
			||||
 | 
					      const settings = await retrieveSettingsForActiveAccount(); | 
				
			||||
 | 
					      this.activeDid = settings.activeDid || ""; | 
				
			||||
 | 
					      this.apiServer = settings.apiServer || ""; | 
				
			||||
 | 
					    } catch (error) { | 
				
			||||
 | 
					      logger.error("Error initializing component:", { | 
				
			||||
 | 
					        error: error instanceof Error ? error.message : String(error), | 
				
			||||
 | 
					        stack: error instanceof Error ? error.stack : undefined, | 
				
			||||
 | 
					      }); | 
				
			||||
 | 
					      this.$notify({ | 
				
			||||
 | 
					        group: "alert", | 
				
			||||
 | 
					        type: "danger", | 
				
			||||
 | 
					        title: "Initialization Error", | 
				
			||||
 | 
					        text: "Failed to initialize QR scanner. Please try again.", | 
				
			||||
 | 
					      }); | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					  } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					  async startScanning() { | 
				
			||||
 | 
					    if (this.isCleaningUp) { | 
				
			||||
 | 
					      logger.debug("Cannot start scanning during cleanup"); | 
				
			||||
 | 
					      return; | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    try { | 
				
			||||
 | 
					      this.error = null; | 
				
			||||
 | 
					      this.isScanning = true; | 
				
			||||
 | 
					      this.lastScannedValue = ""; | 
				
			||||
 | 
					      this.lastScanTime = 0; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					      const scanner = QRScannerFactory.getInstance(); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					      // Check if scanning is supported first | 
				
			||||
 | 
					      if (!(await scanner.isSupported())) { | 
				
			||||
 | 
					        this.error = | 
				
			||||
 | 
					          "Camera access requires HTTPS. Please use a secure connection."; | 
				
			||||
 | 
					        this.isScanning = false; | 
				
			||||
 | 
					        this.$notify( | 
				
			||||
 | 
					          { | 
				
			||||
 | 
					            group: "alert", | 
				
			||||
 | 
					            type: "warning", | 
				
			||||
 | 
					            title: "HTTPS Required", | 
				
			||||
 | 
					            text: "Camera access requires a secure (HTTPS) connection", | 
				
			||||
 | 
					          }, | 
				
			||||
 | 
					          5000, | 
				
			||||
 | 
					        ); | 
				
			||||
 | 
					        return; | 
				
			||||
 | 
					      } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					      // Check permissions first | 
				
			||||
 | 
					      if (!(await scanner.checkPermissions())) { | 
				
			||||
 | 
					        const granted = await scanner.requestPermissions(); | 
				
			||||
 | 
					        if (!granted) { | 
				
			||||
 | 
					          this.error = "Camera permission denied"; | 
				
			||||
 | 
					          this.isScanning = false; | 
				
			||||
 | 
					          // Show notification for better visibility | 
				
			||||
 | 
					          this.$notify( | 
				
			||||
 | 
					            { | 
				
			||||
 | 
					              group: "alert", | 
				
			||||
 | 
					              type: "warning", | 
				
			||||
 | 
					              title: "Camera Access Required", | 
				
			||||
 | 
					              text: "Camera permission denied", | 
				
			||||
 | 
					            }, | 
				
			||||
 | 
					            5000, | 
				
			||||
 | 
					          ); | 
				
			||||
 | 
					          return; | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					      } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					      // Add scan listener | 
				
			||||
 | 
					      scanner.addListener({ | 
				
			||||
 | 
					        onScan: this.onScanDetect, | 
				
			||||
 | 
					        onError: this.onScanError, | 
				
			||||
 | 
					      }); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					      // Start scanning | 
				
			||||
 | 
					      await scanner.startScan(); | 
				
			||||
 | 
					    } catch (error) { | 
				
			||||
 | 
					      this.error = error instanceof Error ? error.message : String(error); | 
				
			||||
 | 
					      this.isScanning = false; | 
				
			||||
 | 
					      logger.error("Error starting scan:", { | 
				
			||||
 | 
					        error: error instanceof Error ? error.message : String(error), | 
				
			||||
 | 
					        stack: error instanceof Error ? error.stack : undefined, | 
				
			||||
 | 
					      }); | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					  } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					  async stopScanning() { | 
				
			||||
 | 
					    try { | 
				
			||||
 | 
					      const scanner = QRScannerFactory.getInstance(); | 
				
			||||
 | 
					      await scanner.stopScan(); | 
				
			||||
 | 
					    } catch (error) { | 
				
			||||
 | 
					      logger.error("Error stopping scan:", { | 
				
			||||
 | 
					        error: error instanceof Error ? error.message : String(error), | 
				
			||||
 | 
					        stack: error instanceof Error ? error.stack : undefined, | 
				
			||||
 | 
					      }); | 
				
			||||
 | 
					    } finally { | 
				
			||||
 | 
					      this.isScanning = false; | 
				
			||||
 | 
					      this.lastScannedValue = ""; | 
				
			||||
 | 
					      this.lastScanTime = 0; | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					  } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					  async cleanupScanner() { | 
				
			||||
 | 
					    if (this.isCleaningUp) { | 
				
			||||
 | 
					      return; | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    this.isCleaningUp = true; | 
				
			||||
 | 
					    try { | 
				
			||||
 | 
					      logger.info("Cleaning up QR scanner resources"); | 
				
			||||
 | 
					      await this.stopScanning(); | 
				
			||||
 | 
					      await QRScannerFactory.cleanup(); | 
				
			||||
 | 
					    } catch (error) { | 
				
			||||
 | 
					      logger.error("Error during scanner cleanup:", { | 
				
			||||
 | 
					        error: error instanceof Error ? error.message : String(error), | 
				
			||||
 | 
					        stack: error instanceof Error ? error.stack : undefined, | 
				
			||||
 | 
					      }); | 
				
			||||
 | 
					    } finally { | 
				
			||||
 | 
					      this.isCleaningUp = false; | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					  } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					  /** | 
				
			||||
 | 
					   * Handle QR code scan result with debouncing to prevent duplicate scans | 
				
			||||
 | 
					   */ | 
				
			||||
 | 
					  async onScanDetect(result: string | QRScanResult) { | 
				
			||||
 | 
					    try { | 
				
			||||
 | 
					      // Extract raw value from different possible formats | 
				
			||||
 | 
					      const rawValue = | 
				
			||||
 | 
					        typeof result === "string" | 
				
			||||
 | 
					          ? result | 
				
			||||
 | 
					          : result?.rawValue || result?.barcode; | 
				
			||||
 | 
					      if (!rawValue) { | 
				
			||||
 | 
					        logger.warn("Invalid scan result - no value found:", result); | 
				
			||||
 | 
					        return; | 
				
			||||
 | 
					      } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					      // Debounce duplicate scans | 
				
			||||
 | 
					      const now = Date.now(); | 
				
			||||
 | 
					      if ( | 
				
			||||
 | 
					        rawValue === this.lastScannedValue && | 
				
			||||
 | 
					        now - this.lastScanTime < this.SCAN_DEBOUNCE_MS | 
				
			||||
 | 
					      ) { | 
				
			||||
 | 
					        logger.info("Ignoring duplicate scan:", rawValue); | 
				
			||||
 | 
					        return; | 
				
			||||
 | 
					      } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					      // Update scan tracking | 
				
			||||
 | 
					      this.lastScannedValue = rawValue; | 
				
			||||
 | 
					      this.lastScanTime = now; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					      logger.info("Processing QR code scan result:", rawValue); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					      // Extract JWT | 
				
			||||
 | 
					      const jwt = getContactJwtFromJwtUrl(rawValue); | 
				
			||||
 | 
					      if (!jwt) { | 
				
			||||
 | 
					        logger.warn("Invalid QR code format - no JWT found in URL"); | 
				
			||||
 | 
					        this.$notify({ | 
				
			||||
 | 
					          group: "alert", | 
				
			||||
 | 
					          type: "danger", | 
				
			||||
 | 
					          title: "Invalid QR Code", | 
				
			||||
 | 
					          text: "This QR code does not contain valid contact information. Please scan a TimeSafari contact QR code.", | 
				
			||||
 | 
					        }); | 
				
			||||
 | 
					        return; | 
				
			||||
 | 
					      } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					      // Process JWT and contact info | 
				
			||||
 | 
					      logger.info("Decoding JWT payload from QR code"); | 
				
			||||
 | 
					      const decodedJwt = await decodeEndorserJwt(jwt); | 
				
			||||
 | 
					      if (!decodedJwt?.payload?.own) { | 
				
			||||
 | 
					        logger.warn("Invalid JWT payload - missing 'own' field"); | 
				
			||||
 | 
					        this.$notify({ | 
				
			||||
 | 
					          group: "alert", | 
				
			||||
 | 
					          type: "danger", | 
				
			||||
 | 
					          title: "Invalid Contact Info", | 
				
			||||
 | 
					          text: "The contact information is incomplete or invalid.", | 
				
			||||
 | 
					        }); | 
				
			||||
 | 
					        return; | 
				
			||||
 | 
					      } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					      const contactInfo = decodedJwt.payload.own; | 
				
			||||
 | 
					      if (!contactInfo.did) { | 
				
			||||
 | 
					        logger.warn("Invalid contact info - missing DID"); | 
				
			||||
 | 
					        this.$notify({ | 
				
			||||
 | 
					          group: "alert", | 
				
			||||
 | 
					          type: "danger", | 
				
			||||
 | 
					          title: "Invalid Contact", | 
				
			||||
 | 
					          text: "The contact DID is missing.", | 
				
			||||
 | 
					        }); | 
				
			||||
 | 
					        return; | 
				
			||||
 | 
					      } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					      // Create contact object | 
				
			||||
 | 
					      const contact = { | 
				
			||||
 | 
					        did: contactInfo.did, | 
				
			||||
 | 
					        name: contactInfo.name || "", | 
				
			||||
 | 
					        email: contactInfo.email || "", | 
				
			||||
 | 
					        phone: contactInfo.phone || "", | 
				
			||||
 | 
					        company: contactInfo.company || "", | 
				
			||||
 | 
					        title: contactInfo.title || "", | 
				
			||||
 | 
					        notes: contactInfo.notes || "", | 
				
			||||
 | 
					      }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					      // Add contact and stop scanning | 
				
			||||
 | 
					      logger.info("Adding new contact to database:", { | 
				
			||||
 | 
					        did: contact.did, | 
				
			||||
 | 
					        name: contact.name, | 
				
			||||
 | 
					      }); | 
				
			||||
 | 
					      await this.addNewContact(contact); | 
				
			||||
 | 
					      await this.stopScanning(); | 
				
			||||
 | 
					      this.$router.back(); // Return to previous view after successful scan | 
				
			||||
 | 
					    } catch (error) { | 
				
			||||
 | 
					      logger.error("Error processing contact QR code:", { | 
				
			||||
 | 
					        error: error instanceof Error ? error.message : String(error), | 
				
			||||
 | 
					        stack: error instanceof Error ? error.stack : undefined, | 
				
			||||
 | 
					      }); | 
				
			||||
 | 
					      this.$notify({ | 
				
			||||
 | 
					        group: "alert", | 
				
			||||
 | 
					        type: "danger", | 
				
			||||
 | 
					        title: "Error", | 
				
			||||
 | 
					        text: | 
				
			||||
 | 
					          error instanceof Error | 
				
			||||
 | 
					            ? error.message | 
				
			||||
 | 
					            : "Could not process QR code. Please try again.", | 
				
			||||
 | 
					      }); | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					  } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					  onScanError(error: Error) { | 
				
			||||
 | 
					    this.error = error.message; | 
				
			||||
 | 
					    logger.error("QR code scan error:", { | 
				
			||||
 | 
					      error: error.message, | 
				
			||||
 | 
					      stack: error.stack, | 
				
			||||
 | 
					    }); | 
				
			||||
 | 
					  } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					  async setVisibility(contact: Contact, visibility: boolean) { | 
				
			||||
 | 
					    const result = await setVisibilityUtil( | 
				
			||||
 | 
					      this.activeDid, | 
				
			||||
 | 
					      this.apiServer, | 
				
			||||
 | 
					      this.axios, | 
				
			||||
 | 
					      db, | 
				
			||||
 | 
					      contact, | 
				
			||||
 | 
					      visibility, | 
				
			||||
 | 
					    ); | 
				
			||||
 | 
					    if (result.error) { | 
				
			||||
 | 
					      this.$notify({ | 
				
			||||
 | 
					        group: "alert", | 
				
			||||
 | 
					        type: "danger", | 
				
			||||
 | 
					        title: "Error Setting Visibility", | 
				
			||||
 | 
					        text: result.error as string, | 
				
			||||
 | 
					      }); | 
				
			||||
 | 
					    } else if (!result.success) { | 
				
			||||
 | 
					      logger.warn("Unexpected result from setting visibility:", result); | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					  } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					  async addNewContact(contact: Contact) { | 
				
			||||
 | 
					    try { | 
				
			||||
 | 
					      logger.info("Opening database connection for new contact"); | 
				
			||||
 | 
					      await db.open(); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					      // Check if contact already exists | 
				
			||||
 | 
					      const existingContacts = await db.contacts.toArray(); | 
				
			||||
 | 
					      const existingContact = existingContacts.find( | 
				
			||||
 | 
					        (c) => c.did === contact.did, | 
				
			||||
 | 
					      ); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					      if (existingContact) { | 
				
			||||
 | 
					        logger.info("Contact already exists", { did: contact.did }); | 
				
			||||
 | 
					        this.$notify( | 
				
			||||
 | 
					          { | 
				
			||||
 | 
					            group: "alert", | 
				
			||||
 | 
					            type: "warning", | 
				
			||||
 | 
					            title: "Contact Exists", | 
				
			||||
 | 
					            text: "This contact has already been added to your list.", | 
				
			||||
 | 
					          }, | 
				
			||||
 | 
					          3000, | 
				
			||||
 | 
					        ); | 
				
			||||
 | 
					        return; | 
				
			||||
 | 
					      } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					      // Add new contact | 
				
			||||
 | 
					      await db.contacts.add(contact); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					      if (this.activeDid) { | 
				
			||||
 | 
					        logger.info("Setting contact visibility", { did: contact.did }); | 
				
			||||
 | 
					        await this.setVisibility(contact, true); | 
				
			||||
 | 
					        contact.seesMe = true; | 
				
			||||
 | 
					      } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					      this.$notify( | 
				
			||||
 | 
					        { | 
				
			||||
 | 
					          group: "alert", | 
				
			||||
 | 
					          type: "success", | 
				
			||||
 | 
					          title: "Contact Added", | 
				
			||||
 | 
					          text: this.activeDid | 
				
			||||
 | 
					            ? "They were added, and your activity is visible to them." | 
				
			||||
 | 
					            : "They were added.", | 
				
			||||
 | 
					        }, | 
				
			||||
 | 
					        3000, | 
				
			||||
 | 
					      ); | 
				
			||||
 | 
					    } catch (error) { | 
				
			||||
 | 
					      logger.error("Error saving contact to database:", { | 
				
			||||
 | 
					        did: contact.did, | 
				
			||||
 | 
					        error: error instanceof Error ? error.message : String(error), | 
				
			||||
 | 
					        stack: error instanceof Error ? error.stack : undefined, | 
				
			||||
 | 
					      }); | 
				
			||||
 | 
					      this.$notify( | 
				
			||||
 | 
					        { | 
				
			||||
 | 
					          group: "alert", | 
				
			||||
 | 
					          type: "danger", | 
				
			||||
 | 
					          title: "Contact Error", | 
				
			||||
 | 
					          text: "Could not save contact. Check if it already exists.", | 
				
			||||
 | 
					        }, | 
				
			||||
 | 
					        5000, | 
				
			||||
 | 
					      ); | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					  } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					  // Lifecycle hooks | 
				
			||||
 | 
					  mounted() { | 
				
			||||
 | 
					    this.isMounted = true; | 
				
			||||
 | 
					    document.addEventListener("pause", this.handleAppPause); | 
				
			||||
 | 
					    document.addEventListener("resume", this.handleAppResume); | 
				
			||||
 | 
					    this.startScanning(); // Automatically start scanning when view is mounted | 
				
			||||
 | 
					  } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					  beforeDestroy() { | 
				
			||||
 | 
					    this.isMounted = false; | 
				
			||||
 | 
					    document.removeEventListener("pause", this.handleAppPause); | 
				
			||||
 | 
					    document.removeEventListener("resume", this.handleAppResume); | 
				
			||||
 | 
					    this.cleanupScanner(); | 
				
			||||
 | 
					  } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					  async handleAppPause() { | 
				
			||||
 | 
					    if (!this.isMounted) return; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    logger.info("App paused, stopping scanner"); | 
				
			||||
 | 
					    await this.stopScanning(); | 
				
			||||
 | 
					  } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					  handleAppResume() { | 
				
			||||
 | 
					    if (!this.isMounted) return; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    logger.info("App resumed, scanner can be restarted by user"); | 
				
			||||
 | 
					    this.isScanning = false; | 
				
			||||
 | 
					  } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					  async handleBack() { | 
				
			||||
 | 
					    await this.cleanupScanner(); | 
				
			||||
 | 
					    this.$router.back(); | 
				
			||||
 | 
					  } | 
				
			||||
 | 
					} | 
				
			||||
 | 
					</script> | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					<style scoped> | 
				
			||||
 | 
					.aspect-square { | 
				
			||||
 | 
					  aspect-ratio: 1 / 1; | 
				
			||||
 | 
					} | 
				
			||||
 | 
					</style> | 
				
			||||
					Loading…
					
					
				
		Reference in new issue