Compare commits
12 Commits
e8d71bfd03
...
feature/co
| Author | SHA1 | Date | |
|---|---|---|---|
| 3579abdecb | |||
| f6ac1b8727 | |||
| eb73398397 | |||
| 1af478d65e | |||
| 7e8a638d01 | |||
| 639004c8c6 | |||
| 17ef39e9a8 | |||
| fff18e05c0 | |||
| c9d0e5ad01 | |||
| 4885d39284 | |||
| 9d42de9fa8 | |||
| bdb7f5e907 |
27
package-lock.json
generated
27
package-lock.json
generated
@@ -17,9 +17,9 @@
|
|||||||
"@angular/router": "^22.0.0",
|
"@angular/router": "^22.0.0",
|
||||||
"@angular/ssr": "^22.0.4",
|
"@angular/ssr": "^22.0.4",
|
||||||
"@fortawesome/fontawesome-free": "^7.2.0",
|
"@fortawesome/fontawesome-free": "^7.2.0",
|
||||||
"angularx-qrcode": "^21.0.5",
|
|
||||||
"bootstrap": "^5.3.8",
|
"bootstrap": "^5.3.8",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
|
"qrcode": "1.5.4",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0"
|
||||||
},
|
},
|
||||||
@@ -3217,6 +3217,17 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@popperjs/core": {
|
||||||
|
"version": "2.11.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||||
|
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/popperjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||||
"version": "4.60.2",
|
"version": "4.60.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz",
|
||||||
@@ -4091,20 +4102,6 @@
|
|||||||
"node": ">= 14.0.0"
|
"node": ">= 14.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/angularx-qrcode": {
|
|
||||||
"version": "21.0.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/angularx-qrcode/-/angularx-qrcode-21.0.5.tgz",
|
|
||||||
"integrity": "sha512-9Kv/Mi5tKzAkBL2xhYZyPaJEoq+b24mIpUAntp16kIBiozVnpGI6DbigTPJiSDkV7EpYHdjBJNGj8igFAHEp6g==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"qrcode": "1.5.4",
|
|
||||||
"tslib": "^2.3.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@angular/common": "^21.0.0",
|
|
||||||
"@angular/core": "^21.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ansi-escapes": {
|
"node_modules/ansi-escapes": {
|
||||||
"version": "7.3.0",
|
"version": "7.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz",
|
||||||
|
|||||||
@@ -21,9 +21,9 @@
|
|||||||
"@angular/router": "^22.0.0",
|
"@angular/router": "^22.0.0",
|
||||||
"@angular/ssr": "^22.0.4",
|
"@angular/ssr": "^22.0.4",
|
||||||
"@fortawesome/fontawesome-free": "^7.2.0",
|
"@fortawesome/fontawesome-free": "^7.2.0",
|
||||||
"angularx-qrcode": "^21.0.5",
|
|
||||||
"bootstrap": "^5.3.8",
|
"bootstrap": "^5.3.8",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
|
"qrcode": "1.5.4",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -40,12 +40,24 @@
|
|||||||
title="Carrito"
|
title="Carrito"
|
||||||
(click)="cartClick.emit()"
|
(click)="cartClick.emit()"
|
||||||
/>
|
/>
|
||||||
<app-icon-button
|
<div class="store-layout__user-menu">
|
||||||
variant="user"
|
<app-icon-button
|
||||||
ariaLabel="Mi cuenta"
|
variant="user"
|
||||||
title="Mi cuenta"
|
ariaLabel="Mi cuenta"
|
||||||
(click)="userClick.emit()"
|
ariaControls="store-user-dropdown"
|
||||||
/>
|
[ariaExpanded]="isUserDropdownOpen()"
|
||||||
|
title="Mi cuenta"
|
||||||
|
(clicked)="toggleUserDropdown()"
|
||||||
|
/>
|
||||||
|
|
||||||
|
@if (isUserDropdownOpen()) {
|
||||||
|
<app-user-dropdown
|
||||||
|
id="store-user-dropdown"
|
||||||
|
[user]="user()"
|
||||||
|
(logoutClick)="onLogoutClick()"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -83,6 +83,11 @@
|
|||||||
color: #8a8a8a;
|
color: #8a8a8a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.store-layout__user-menu {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
.store-layout__category-trigger {
|
.store-layout__category-trigger {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.02em;
|
||||||
|
|||||||
@@ -1,17 +1,62 @@
|
|||||||
import { Component, input, output } from '@angular/core';
|
import { Component, ElementRef, HostListener, inject, input, output, signal } from '@angular/core';
|
||||||
import { RouterLink } from '@angular/router';
|
import { RouterLink } from '@angular/router';
|
||||||
|
import { AuthUser } from '../../../services/auth/auth.interfaces';
|
||||||
import { CartIconComponent } from '../../../../shared/components/cart-icon/cart-icon.component';
|
import { CartIconComponent } from '../../../../shared/components/cart-icon/cart-icon.component';
|
||||||
import { IconButtonComponent } from '../../../../shared/components/icon-button/icon-button.component';
|
import { IconButtonComponent } from '../../../../shared/components/icon-button/icon-button.component';
|
||||||
|
import { UserDropdownComponent } from './user-dropdown/user-dropdown.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-store-header',
|
selector: 'app-store-header',
|
||||||
imports: [CartIconComponent, IconButtonComponent, RouterLink],
|
imports: [CartIconComponent, IconButtonComponent, RouterLink, UserDropdownComponent],
|
||||||
templateUrl: './store-header.component.html',
|
templateUrl: './store-header.component.html',
|
||||||
styleUrl: './store-header.component.scss'
|
styleUrl: './store-header.component.scss'
|
||||||
})
|
})
|
||||||
export class StoreHeaderComponent {
|
export class StoreHeaderComponent {
|
||||||
|
private readonly elementRef = inject(ElementRef<HTMLElement>);
|
||||||
|
|
||||||
readonly logoUrl = input<string | null>(null);
|
readonly logoUrl = input<string | null>(null);
|
||||||
readonly cartQuantity = input<number>(0);
|
readonly cartQuantity = input<number>(0);
|
||||||
|
readonly user = input<AuthUser | null>(null);
|
||||||
|
readonly isAuthenticated = input<boolean>(false);
|
||||||
readonly cartClick = output<void>();
|
readonly cartClick = output<void>();
|
||||||
readonly userClick = output<void>();
|
readonly loginClick = output<void>();
|
||||||
|
readonly logoutClick = output<void>();
|
||||||
|
|
||||||
|
protected readonly isUserDropdownOpen = signal(false);
|
||||||
|
|
||||||
|
@HostListener('document:click', ['$event'])
|
||||||
|
protected onDocumentClick(event: MouseEvent): void {
|
||||||
|
if (!this.isUserDropdownOpen()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = event.target;
|
||||||
|
if (target instanceof Node && !this.elementRef.nativeElement.contains(target)) {
|
||||||
|
this.isUserDropdownOpen.set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@HostListener('document:keydown.escape')
|
||||||
|
protected onEscapeKey(): void {
|
||||||
|
this.isUserDropdownOpen.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected toggleUserDropdown(): void {
|
||||||
|
if (!this.isAuthenticated()) {
|
||||||
|
this.loginClick.emit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isUserDropdownOpen.update((isOpen) => !isOpen);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onLoginClick(): void {
|
||||||
|
this.isUserDropdownOpen.set(false);
|
||||||
|
this.loginClick.emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onLogoutClick(): void {
|
||||||
|
this.isUserDropdownOpen.set(false);
|
||||||
|
this.logoutClick.emit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<section class="user-dropdown" role="menu" aria-label="Menu de usuario">
|
||||||
|
<div class="user-dropdown__profile">
|
||||||
|
<span class="user-dropdown__name">{{ displayName() }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="user-dropdown__divider"></div>
|
||||||
|
|
||||||
|
<button type="button" class="user-dropdown__item" role="menuitem" data-testid="user-dropdown-account">
|
||||||
|
Mi cuenta
|
||||||
|
</button>
|
||||||
|
<button type="button" class="user-dropdown__item" role="menuitem" data-testid="user-dropdown-purchases">
|
||||||
|
Mis compras
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="user-dropdown__item"
|
||||||
|
role="menuitem"
|
||||||
|
data-testid="user-dropdown-logout"
|
||||||
|
(click)="logoutClick.emit()"
|
||||||
|
>
|
||||||
|
Cerrar sesion
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
:host {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 0.75rem);
|
||||||
|
right: 0;
|
||||||
|
z-index: 1060;
|
||||||
|
display: block;
|
||||||
|
width: min(18rem, calc(100vw - 2rem));
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-dropdown {
|
||||||
|
display: grid;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid rgba(32, 32, 32, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 18px 44px rgba(0, 0, 0, 0.16);
|
||||||
|
padding: 0.75rem;
|
||||||
|
animation: user-dropdown-enter 0.16s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-dropdown__profile {
|
||||||
|
padding: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-dropdown__name {
|
||||||
|
color: #a0a0a0;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 325;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-dropdown__divider {
|
||||||
|
height: 1px;
|
||||||
|
margin: 0.6rem 0;
|
||||||
|
background: rgba(32, 32, 32, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-dropdown__item {
|
||||||
|
width: 100%;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.65rem 0.6rem;
|
||||||
|
color: #8a8a8a;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: left;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-dropdown__item:active,
|
||||||
|
.user-dropdown__item:hover,
|
||||||
|
.user-dropdown__item:focus-visible {
|
||||||
|
color: var(--tenant-primary);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes user-dropdown-enter {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-0.35rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 575.98px) {
|
||||||
|
:host {
|
||||||
|
right: -0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { Component, computed, input, output } from '@angular/core';
|
||||||
|
import { AuthUser } from '../../../../services/auth/auth.interfaces';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-user-dropdown',
|
||||||
|
templateUrl: './user-dropdown.component.html',
|
||||||
|
styleUrl: './user-dropdown.component.scss'
|
||||||
|
})
|
||||||
|
export class UserDropdownComponent {
|
||||||
|
readonly user = input<AuthUser | null>(null);
|
||||||
|
readonly logoutClick = output<void>();
|
||||||
|
|
||||||
|
protected readonly displayName = computed(() => this.user()?.nombre_apellido || 'Mi cuenta');
|
||||||
|
}
|
||||||
@@ -2,8 +2,11 @@
|
|||||||
<app-store-header
|
<app-store-header
|
||||||
[cartQuantity]="cartQuantity()"
|
[cartQuantity]="cartQuantity()"
|
||||||
[logoUrl]="tenant()?.header_logo ?? null"
|
[logoUrl]="tenant()?.header_logo ?? null"
|
||||||
|
[user]="user()"
|
||||||
|
[isAuthenticated]="isAuthenticated()"
|
||||||
(cartClick)="isCartOpen.set(!isCartOpen())"
|
(cartClick)="isCartOpen.set(!isCartOpen())"
|
||||||
(userClick)="onUserClick()"
|
(loginClick)="onLoginClick()"
|
||||||
|
(logoutClick)="onLogoutClick()"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@if (isCartOpen()) {
|
@if (isCartOpen()) {
|
||||||
|
|||||||
@@ -24,7 +24,6 @@
|
|||||||
top: 76px;
|
top: 76px;
|
||||||
right: calc((100% - 1320px) / 2 + 1.5rem);
|
right: calc((100% - 1320px) / 2 + 1.5rem);
|
||||||
|
|
||||||
height: 520px;
|
|
||||||
max-height: calc(100vh - 120px);
|
max-height: calc(100vh - 120px);
|
||||||
z-index: 1050;
|
z-index: 1050;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { TenantService } from '../../services/tenant.service';
|
|||||||
import { CartService } from '../../services/cart/cart.service';
|
import { CartService } from '../../services/cart/cart.service';
|
||||||
import { ToastService } from '../../services/toast.service';
|
import { ToastService } from '../../services/toast.service';
|
||||||
import { AuthService } from '../../services/auth/auth.service';
|
import { AuthService } from '../../services/auth/auth.service';
|
||||||
|
import { AuthUser } from '../../services/auth/auth.interfaces';
|
||||||
import { StoreLayoutComponent } from './store-layout.component';
|
import { StoreLayoutComponent } from './store-layout.component';
|
||||||
|
|
||||||
const tenant: Tenant = {
|
const tenant: Tenant = {
|
||||||
@@ -30,6 +31,8 @@ const tenant: Tenant = {
|
|||||||
describe('StoreLayoutComponent', () => {
|
describe('StoreLayoutComponent', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const tenantState = signal(tenant);
|
const tenantState = signal(tenant);
|
||||||
|
const authUserState = signal<AuthUser | null>(null);
|
||||||
|
const isAuthenticatedState = signal(false);
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [StoreLayoutComponent],
|
imports: [StoreLayoutComponent],
|
||||||
@@ -66,7 +69,9 @@ describe('StoreLayoutComponent', () => {
|
|||||||
{
|
{
|
||||||
provide: AuthService,
|
provide: AuthService,
|
||||||
useValue: {
|
useValue: {
|
||||||
isAuthenticated: signal(false)
|
user: authUserState,
|
||||||
|
isAuthenticated: isAuthenticatedState,
|
||||||
|
logout: vi.fn().mockReturnValue(of(void 0))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -112,7 +117,6 @@ describe('StoreLayoutComponent', () => {
|
|||||||
const router = TestBed.inject(Router);
|
const router = TestBed.inject(Router);
|
||||||
vi.spyOn(router, 'navigate');
|
vi.spyOn(router, 'navigate');
|
||||||
|
|
||||||
// Set isAuthenticated to false
|
|
||||||
(authService.isAuthenticated as any).set(false);
|
(authService.isAuthenticated as any).set(false);
|
||||||
|
|
||||||
const fixture = TestBed.createComponent(StoreLayoutComponent);
|
const fixture = TestBed.createComponent(StoreLayoutComponent);
|
||||||
@@ -124,16 +128,21 @@ describe('StoreLayoutComponent', () => {
|
|||||||
userButton.click();
|
userButton.click();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(compiled.querySelector('app-user-dropdown')).toBeNull();
|
||||||
expect(router.navigate).toHaveBeenCalledWith(['/login']);
|
expect(router.navigate).toHaveBeenCalledWith(['/login']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not redirect to /login when user icon is clicked and user is logged in', () => {
|
it('opens authenticated user dropdown without redirecting on icon click', () => {
|
||||||
const authService = TestBed.inject(AuthService);
|
const authService = TestBed.inject(AuthService);
|
||||||
const router = TestBed.inject(Router);
|
const router = TestBed.inject(Router);
|
||||||
vi.spyOn(router, 'navigate');
|
vi.spyOn(router, 'navigate');
|
||||||
|
|
||||||
// Set isAuthenticated to true
|
|
||||||
(authService.isAuthenticated as any).set(true);
|
(authService.isAuthenticated as any).set(true);
|
||||||
|
(authService.user as any).set({
|
||||||
|
id: 1,
|
||||||
|
nombre_apellido: 'Ada Lovelace',
|
||||||
|
email: 'ada@example.com'
|
||||||
|
});
|
||||||
|
|
||||||
const fixture = TestBed.createComponent(StoreLayoutComponent);
|
const fixture = TestBed.createComponent(StoreLayoutComponent);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
@@ -144,9 +153,40 @@ describe('StoreLayoutComponent', () => {
|
|||||||
userButton.click();
|
userButton.click();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(compiled.querySelector('app-user-dropdown')).not.toBeNull();
|
||||||
|
expect(compiled.textContent).toContain('Ada Lovelace');
|
||||||
expect(router.navigate).not.toHaveBeenCalled();
|
expect(router.navigate).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('logs out from the authenticated user dropdown', () => {
|
||||||
|
const authService = TestBed.inject(AuthService);
|
||||||
|
const router = TestBed.inject(Router);
|
||||||
|
vi.spyOn(router, 'navigate');
|
||||||
|
|
||||||
|
(authService.isAuthenticated as any).set(true);
|
||||||
|
(authService.user as any).set({
|
||||||
|
id: 1,
|
||||||
|
nombre_apellido: 'Ada Lovelace',
|
||||||
|
email: 'ada@example.com'
|
||||||
|
});
|
||||||
|
|
||||||
|
const fixture = TestBed.createComponent(StoreLayoutComponent);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const compiled = fixture.nativeElement as HTMLElement;
|
||||||
|
const userButton = compiled.querySelector('app-icon-button[variant="user"] button') as HTMLButtonElement;
|
||||||
|
userButton.click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const logoutButton = compiled.querySelector('[data-testid="user-dropdown-logout"]') as HTMLButtonElement;
|
||||||
|
expect(logoutButton).not.toBeNull();
|
||||||
|
logoutButton.click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(authService.logout).toHaveBeenCalled();
|
||||||
|
expect(router.navigate).toHaveBeenCalledWith(['/login']);
|
||||||
|
});
|
||||||
|
|
||||||
it('redirects to /checkout when cart buy button is clicked', () => {
|
it('redirects to /checkout when cart buy button is clicked', () => {
|
||||||
const router = TestBed.inject(Router);
|
const router = TestBed.inject(Router);
|
||||||
vi.spyOn(router, 'navigate');
|
vi.spyOn(router, 'navigate');
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ export class StoreLayoutComponent implements OnInit {
|
|||||||
|
|
||||||
protected readonly currentYear = new Date().getFullYear();
|
protected readonly currentYear = new Date().getFullYear();
|
||||||
protected readonly tenant = this.tenantService.tenant;
|
protected readonly tenant = this.tenantService.tenant;
|
||||||
|
protected readonly user = this.authService.user;
|
||||||
|
protected readonly isAuthenticated = this.authService.isAuthenticated;
|
||||||
|
|
||||||
protected readonly cartQuantity = computed(() => {
|
protected readonly cartQuantity = computed(() => {
|
||||||
const cart = this.cartService.cart();
|
const cart = this.cartService.cart();
|
||||||
@@ -83,10 +85,15 @@ export class StoreLayoutComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected onUserClick(): void {
|
protected onLoginClick(): void {
|
||||||
if (!this.authService.isAuthenticated()) {
|
void this.router.navigate(['/login']);
|
||||||
this.router.navigate(['/login']);
|
}
|
||||||
}
|
|
||||||
|
protected onLogoutClick(): void {
|
||||||
|
this.authService.logout().subscribe({
|
||||||
|
next: () => void this.router.navigate(['/login']),
|
||||||
|
error: (err) => console.error('Error logging out', err)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected onCheckoutClick(): void {
|
protected onCheckoutClick(): void {
|
||||||
|
|||||||
@@ -454,6 +454,7 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -652,3 +653,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</app-store-section>
|
</app-store-section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
@@ -113,8 +113,8 @@ h1 {
|
|||||||
|
|
||||||
.cart-showcase__viewport {
|
.cart-showcase__viewport {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 370px;
|
max-width: 504px;
|
||||||
height: 360px;
|
height: 640px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cart-showcase__header {
|
.cart-showcase__header {
|
||||||
|
|||||||
@@ -257,3 +257,5 @@ export class ReutilizablesTestPageComponent {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,19 +9,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__actions {
|
&__actions {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: 1fr 1fr;
|
justify-content: flex-end;
|
||||||
|
flex-wrap: wrap;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
|
|
||||||
@media (max-width: 540px) {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__action-btn {
|
&__action-btn {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
flex: 1 1 290px;
|
||||||
|
max-width: 290px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,3 +45,4 @@
|
|||||||
color: var(--bs-danger, #dc3545);
|
color: var(--bs-danger, #dc3545);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<div class="container-xl px-3 px-md-4 py-4">
|
<div class="container-xl px-3 px-md-4 py-4">
|
||||||
<div class="checkout-page">
|
<div class="checkout-page">
|
||||||
<div class="checkout-page__stepper-col">
|
<div class="checkout-page__stepper-col ">
|
||||||
<app-stepper #stepper>
|
<app-stepper #stepper>
|
||||||
<app-step label="Datos" [isValid]="isStep1Valid()">
|
<app-step label="Datos" [isValid]="isStep1Valid()">
|
||||||
<app-checkout-data-step
|
<app-checkout-data-step
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
.checkout-page {
|
.checkout-page {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) 420px;
|
grid-template-columns: minmax(0, 1fr) 420px;
|
||||||
gap: 2rem;
|
gap: 4rem;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
padding: 2rem 0;
|
padding: 2rem 0;
|
||||||
|
|
||||||
@@ -16,9 +16,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__cart-col {
|
&__cart-col {
|
||||||
border: 1px solid #ececec;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 1.25rem 1.5rem;
|
|
||||||
max-height: 80vh;
|
max-height: 80vh;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="payment-panel" aria-live="polite">
|
<section class="payment-panel d-flex justify-content-end" aria-live="polite">
|
||||||
@if (isGeneratingIntent()) {
|
@if (isGeneratingIntent()) {
|
||||||
<div class="payment-panel__card" style="text-align: center; padding: 2rem;">
|
<div class="payment-panel__card" style="text-align: center; padding: 2rem;">
|
||||||
<h3 class="payment-panel__title">Cargando información de pago...</h3>
|
<h3 class="payment-panel__title">Cargando información de pago...</h3>
|
||||||
@@ -133,7 +133,8 @@
|
|||||||
type="button"
|
type="button"
|
||||||
variant="cancel"
|
variant="cancel"
|
||||||
hostClass="checkout-payment__action-btn"
|
hostClass="checkout-payment__action-btn"
|
||||||
(click)="cancelStep.emit()"
|
buttonClass="w-100"
|
||||||
|
[disabled]="true"
|
||||||
>
|
>
|
||||||
Cancelar
|
Cancelar
|
||||||
</app-button>
|
</app-button>
|
||||||
@@ -141,6 +142,7 @@
|
|||||||
<app-button
|
<app-button
|
||||||
type="button"
|
type="button"
|
||||||
hostClass="checkout-payment__action-btn"
|
hostClass="checkout-payment__action-btn"
|
||||||
|
buttonClass="w-100"
|
||||||
[disabled]="isGeneratingIntent()"
|
[disabled]="isGeneratingIntent()"
|
||||||
(click)="complete.emit()"
|
(click)="complete.emit()"
|
||||||
>
|
>
|
||||||
@@ -148,3 +150,4 @@
|
|||||||
</app-button>
|
</app-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -14,17 +14,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__actions {
|
&__actions {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: 1fr 1fr;
|
justify-content: flex-end;
|
||||||
|
flex-wrap: wrap;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
|
|
||||||
@media (max-width: 540px) {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__action-btn {
|
&__action-btn {
|
||||||
|
width: 100%;
|
||||||
|
flex: 1 1 290px;
|
||||||
|
max-width: 290px;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -110,10 +110,9 @@
|
|||||||
|
|
||||||
.payment-panel {
|
.payment-panel {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
||||||
&__card {
|
&__card {
|
||||||
width: fit-content;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 290px;
|
||||||
min-height: 269px;
|
min-height: 269px;
|
||||||
padding: 1.5rem 1.75rem;
|
padding: 1.5rem 1.75rem;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
@@ -304,3 +303,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { ChangeDetectionStrategy, Component, input, output } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, input, output } from '@angular/core';
|
||||||
|
|
||||||
import { QRCodeComponent } from 'angularx-qrcode';
|
|
||||||
|
|
||||||
import { ButtonComponent } from '../../../../shared/components/button/button.component';
|
import { ButtonComponent } from '../../../../shared/components/button/button.component';
|
||||||
import { IconButtonComponent } from '../../../../shared/components/icon-button/icon-button.component';
|
import { IconButtonComponent } from '../../../../shared/components/icon-button/icon-button.component';
|
||||||
|
import { QRCodeComponent } from '../../../../shared/components/qrcode/qrcode.component';
|
||||||
import { PaymentMethod, PaymentMethodOption, TransferAccount, TransferField } from './checkout-page.models';
|
import { PaymentMethod, PaymentMethodOption, TransferAccount, TransferField } from './checkout-page.models';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ export type ButtonVariant =
|
|||||||
styleUrl: './button.component.scss',
|
styleUrl: './button.component.scss',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
host: {
|
host: {
|
||||||
'[class]': 'hostClass()'
|
'[class]': 'hostClasses()',
|
||||||
|
'[attr.aria-disabled]': 'disabled()',
|
||||||
|
'(click)': 'onHostClick($event)'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
export class ButtonComponent {
|
export class ButtonComponent {
|
||||||
@@ -26,6 +28,9 @@ export class ButtonComponent {
|
|||||||
readonly disabled = input(false);
|
readonly disabled = input(false);
|
||||||
readonly hostClass = input<string>('');
|
readonly hostClass = input<string>('');
|
||||||
readonly buttonClass = input<string>('');
|
readonly buttonClass = input<string>('');
|
||||||
|
protected readonly hostClasses = computed(() =>
|
||||||
|
[this.hostClass(), this.disabled() ? 'app-button--disabled' : ''].filter(Boolean).join(' ')
|
||||||
|
);
|
||||||
|
|
||||||
protected readonly buttonClasses = computed(() => {
|
protected readonly buttonClasses = computed(() => {
|
||||||
const variants: Record<ButtonVariant, string> = {
|
const variants: Record<ButtonVariant, string> = {
|
||||||
@@ -40,4 +45,14 @@ export class ButtonComponent {
|
|||||||
|
|
||||||
return ['btn', variants[this.variant()], this.buttonClass()];
|
return ['btn', variants[this.variant()], this.buttonClass()];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
protected onHostClick(event: Event): void {
|
||||||
|
if (!this.disabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
event.stopImmediatePropagation();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<article class="w-100 rounded-0 cart-item">
|
<article class="w-100 p-3 rounded-0 cart-item">
|
||||||
<div class="position-relative overflow-hidden cart-item-media">
|
<div class="position-relative overflow-hidden cart-item-media">
|
||||||
@if (discountPercentage() && discountPercentage()! > 0) {
|
@if (discountPercentage() && discountPercentage()! > 0) {
|
||||||
<span class="position-absolute top-0 start-0 z-1 rounded-0 cart-item-discount-badge">-{{ discountPercentage() }}%</span>
|
<span class="position-absolute top-0 start-0 z-1 rounded-0 cart-item-discount-badge">-{{ discountPercentage() }}%</span>
|
||||||
@@ -17,10 +17,10 @@
|
|||||||
|
|
||||||
<div class="text-nowrap cart-item-prices">
|
<div class="text-nowrap cart-item-prices">
|
||||||
@if (formattedOriginalPrice(); as originalPrice) {
|
@if (formattedOriginalPrice(); as originalPrice) {
|
||||||
<span class="text-decoration-line-through fw-light cart-item-original-price">{{ originalPrice }}</span>
|
<span class="text-decoration-line-through mb-1 fw-light cart-item-original-price">{{ originalPrice }}</span>
|
||||||
}
|
}
|
||||||
|
|
||||||
<span class="fw-normal cart-item-discounted-price">{{ formattedDiscountedPrice() }}</span>
|
<span class="fw-bold cart-item-discounted-price">{{ formattedDiscountedPrice() }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +1,43 @@
|
|||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
--item-media-width: 64px;
|
--item-media-size: 90px;
|
||||||
--item-media-height: 72px;
|
--item-divider-inset: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 420px) {
|
:host(:first-child) .cart-item,
|
||||||
:host {
|
:host .cart-item {
|
||||||
--item-media-width: 56px;
|
position: relative;
|
||||||
--item-media-height: 64px;
|
}
|
||||||
}
|
|
||||||
|
:host(:first-child) .cart-item::before,
|
||||||
|
:host .cart-item::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
right: var(--item-divider-inset);
|
||||||
|
left: var(--item-divider-inset);
|
||||||
|
height: 1px;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host(:first-child) .cart-item::before {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host .cart-item::after {
|
||||||
|
bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cart-item {
|
.cart-item {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: var(--item-media-width) minmax(0, 1fr);
|
grid-template-columns: var(--item-media-size) minmax(0, 1fr);
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
padding: 10px 12px;
|
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cart-item-media {
|
.cart-item-media {
|
||||||
width: var(--item-media-width);
|
width: var(--item-media-size);
|
||||||
height: var(--item-media-height);
|
aspect-ratio: 1 / 1;
|
||||||
border-radius: 6px;
|
border-radius: 5px 0 0 5px;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
border: 1px solid #eaeaea;
|
border: 1px solid #eaeaea;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -30,14 +45,14 @@
|
|||||||
img {
|
img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: contain;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.cart-item-discount-badge {
|
.cart-item-discount-badge {
|
||||||
padding: 2px 5px;
|
padding: 2px 5px;
|
||||||
border-bottom-right-radius: 4px;
|
border-bottom-right-radius: 4px;
|
||||||
background-color: #6173ff;
|
background-color: var(--bs-primary);
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
font-size: 8px;
|
font-size: 8px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -45,6 +60,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.cart-item-placeholder {
|
.cart-item-placeholder {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
background: linear-gradient(135deg, #d2d2d2, #ececec);
|
background: linear-gradient(135deg, #d2d2d2, #ececec);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,9 +117,6 @@
|
|||||||
gap: 0.125rem;
|
gap: 0.125rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.cart-item-remove-btn {
|
.cart-item-remove-btn {
|
||||||
margin-left: 0.35rem;
|
margin-left: 0.35rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<section class="d-flex flex-column h-100 overflow-hidden text-secondary" [style.background-color]="backgroundColor()">
|
<section class="d-flex flex-column h-100 overflow-hidden text-secondary" [style.background-color]="backgroundColor()">
|
||||||
<header class="d-flex align-items-center justify-content-between bg-transparent cart-header">
|
<header class="d-flex align-items-center p-3 justify-content-between bg-transparent cart-header">
|
||||||
<h2 class="m-0 text-uppercase fw-bold cart-title">{{ title() }}</h2>
|
<h2 class="m-0 text-uppercase fw-bold cart-title">{{ title() }}</h2>
|
||||||
|
|
||||||
@if (showClose()) {
|
@if (showClose()) {
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
}
|
}
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="d-grid gap-2 flex-grow-1 overflow-y-auto cart-items-container">
|
<div class="d-grid w-100 flex-grow-1 overflow-y-auto cart-items-container">
|
||||||
@for (item of items(); track item.productVariantId || item.product + item.discountedPrice; let idx = $index) {
|
@for (item of items(); track item.productVariantId || item.product + item.discountedPrice; let idx = $index) {
|
||||||
<app-cart-item
|
<app-cart-item
|
||||||
[imageUrl]="item.imageUrl"
|
[imageUrl]="item.imageUrl"
|
||||||
@@ -48,3 +48,4 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -23,10 +23,16 @@
|
|||||||
|
|
||||||
.cart-items-container {
|
.cart-items-container {
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
padding: 0 0 0 24px;
|
gap: 0;
|
||||||
scrollbar-color: #d5d5d5 transparent;
|
scrollbar-color: #d5d5d5 transparent;
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
|
|
||||||
|
app-cart-item {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
width: 6px;
|
width: 6px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
[disabled]="disabled()"
|
[disabled]="disabled()"
|
||||||
[ngClass]="buttonClasses()"
|
[ngClass]="buttonClasses()"
|
||||||
[attr.aria-label]="ariaLabel() || variant()"
|
[attr.aria-label]="ariaLabel() || variant()"
|
||||||
|
[attr.aria-expanded]="ariaExpanded()"
|
||||||
|
[attr.aria-controls]="ariaControls()"
|
||||||
|
(click)="clicked.emit($event)"
|
||||||
>
|
>
|
||||||
<i [ngClass]="iconClass()"></i>
|
<i [ngClass]="iconClass()"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, computed, input, output } from '@angular/core';
|
||||||
import { NgClass } from '@angular/common';
|
import { NgClass } from '@angular/common';
|
||||||
|
|
||||||
export type IconButtonVariant =
|
export type IconButtonVariant =
|
||||||
@@ -21,6 +21,9 @@ export class IconButtonComponent {
|
|||||||
readonly variant = input.required<IconButtonVariant>();
|
readonly variant = input.required<IconButtonVariant>();
|
||||||
readonly disabled = input(false);
|
readonly disabled = input(false);
|
||||||
readonly ariaLabel = input<string>('');
|
readonly ariaLabel = input<string>('');
|
||||||
|
readonly ariaExpanded = input<boolean | null>(null);
|
||||||
|
readonly ariaControls = input<string | null>(null);
|
||||||
|
readonly clicked = output<MouseEvent>();
|
||||||
|
|
||||||
protected readonly iconClass = computed(() => {
|
protected readonly iconClass = computed(() => {
|
||||||
const classes: Record<IconButtonVariant, string> = {
|
const classes: Record<IconButtonVariant, string> = {
|
||||||
|
|||||||
68
src/app/shared/components/qrcode/qrcode.component.ts
Normal file
68
src/app/shared/components/qrcode/qrcode.component.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import {
|
||||||
|
AfterViewInit,
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
ElementRef,
|
||||||
|
Input,
|
||||||
|
OnChanges,
|
||||||
|
ViewChild
|
||||||
|
} from '@angular/core';
|
||||||
|
|
||||||
|
import * as QRCode from 'qrcode';
|
||||||
|
|
||||||
|
type QRCodeErrorCorrectionLevel = 'L' | 'M' | 'Q' | 'H';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'qrcode',
|
||||||
|
standalone: true,
|
||||||
|
template: '<canvas #canvas [attr.aria-label]="ariaLabel" role="img"></canvas>',
|
||||||
|
styles: [
|
||||||
|
`
|
||||||
|
:host {
|
||||||
|
display: inline-block;
|
||||||
|
line-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
display: block;
|
||||||
|
height: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class QRCodeComponent implements AfterViewInit, OnChanges {
|
||||||
|
@Input({ required: true }) qrdata = '';
|
||||||
|
@Input() width = 200;
|
||||||
|
@Input() errorCorrectionLevel: QRCodeErrorCorrectionLevel = 'M';
|
||||||
|
@Input() ariaLabel = 'Codigo QR';
|
||||||
|
|
||||||
|
@ViewChild('canvas', { static: true }) private readonly canvas!: ElementRef<HTMLCanvasElement>;
|
||||||
|
|
||||||
|
private viewReady = false;
|
||||||
|
|
||||||
|
ngAfterViewInit(): void {
|
||||||
|
this.viewReady = true;
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnChanges(): void {
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
private render(): void {
|
||||||
|
if (!this.viewReady || !this.qrdata) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
void QRCode.toCanvas(this.canvas.nativeElement, this.qrdata, {
|
||||||
|
errorCorrectionLevel: this.errorCorrectionLevel,
|
||||||
|
margin: 2,
|
||||||
|
width: this.width
|
||||||
|
}).catch(() => {
|
||||||
|
const context = this.canvas.nativeElement.getContext('2d');
|
||||||
|
context?.clearRect(0, 0, this.canvas.nativeElement.width, this.canvas.nativeElement.height);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/types/qrcode.d.ts
vendored
Normal file
15
src/types/qrcode.d.ts
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
declare module 'qrcode' {
|
||||||
|
export type QRCodeErrorCorrectionLevel = 'L' | 'M' | 'Q' | 'H';
|
||||||
|
|
||||||
|
export interface QRCodeToCanvasOptions {
|
||||||
|
errorCorrectionLevel?: QRCodeErrorCorrectionLevel;
|
||||||
|
margin?: number;
|
||||||
|
width?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toCanvas(
|
||||||
|
canvasElement: HTMLCanvasElement,
|
||||||
|
text: string,
|
||||||
|
options?: QRCodeToCanvasOptions
|
||||||
|
): Promise<void>;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user