diff --git a/src/app/app.component.html b/src/app/app.component.html
index 938ca4c..f965570 100644
--- a/src/app/app.component.html
+++ b/src/app/app.component.html
@@ -50,3 +50,7 @@
+
+
+ {{ snackBarText() }}
+
diff --git a/src/app/app.component.scss b/src/app/app.component.scss
index b1b429f..4f7c6bc 100644
--- a/src/app/app.component.scss
+++ b/src/app/app.component.scss
@@ -22,3 +22,17 @@ header {
display: flex;
}
}
+
+.snack-bar {
+ background: rgba(255, 255, 255, 0.2);
+ border-radius: 0.5rem;
+ bottom: 2rem;
+ opacity: 0;
+ padding: 1rem;
+ position: absolute;
+ transition: opacity 0.25s;
+
+ &.visible {
+ opacity: 1;
+ }
+}
diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts
index 6de6df4..4e995ce 100644
--- a/src/app/app.component.spec.ts
+++ b/src/app/app.component.spec.ts
@@ -21,6 +21,15 @@ describe('AppComponent', () => {
).and.returnValue(true);
};
+ beforeAll(() => {
+ jasmine.DEFAULT_TIMEOUT_INTERVAL = 10_000;
+ jasmine.clock().install();
+ });
+
+ afterAll(() => {
+ jasmine.clock().uninstall();
+ });
+
beforeEach(async () => {
const maze = new Maze(4, new Chooser(42));
gameStateService = jasmine.createSpyObj(
@@ -190,5 +199,38 @@ describe('AppComponent', () => {
'http://example.com/?seed=321',
);
});
+
+ it('should show a snack bar confirming that the URL was copied', (done) => {
+ fixture.debugElement
+ .query(By.css('[data-test-id="share-maze-button"]'))
+ .nativeElement.click();
+ fixture.detectChanges();
+
+ expect(
+ fixture.nativeElement.querySelector('.snack-bar').textContent.trim(),
+ ).toBe('URL copied to clipboard');
+ expect(
+ fixture.nativeElement
+ .querySelector('.snack-bar')
+ .classList.contains('visible'),
+ ).toBeTrue();
+
+ // check that the snackbar is hidden after 2s and emptied after 3s
+
+ jasmine.clock().tick(2000);
+ fixture.detectChanges();
+ expect(
+ fixture.nativeElement.querySelector('.snack-bar').style.opacity,
+ ).toBe('0');
+
+ jasmine.clock().tick(1000);
+ fixture.detectChanges();
+ expect(
+ fixture.nativeElement
+ .querySelector('.snack-bar')
+ .classList.contains('visible'),
+ ).toBeFalse();
+ done();
+ });
});
});
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index 7eb0da3..a3075a8 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -4,6 +4,8 @@ import {
inject,
InjectionToken,
OnInit,
+ signal,
+ ElementRef,
} from '@angular/core';
import { MazeComponent } from './maze/maze.component';
@@ -29,6 +31,7 @@ export const WINDOW_TOKEN = new InjectionToken('window', {
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent implements OnInit {
+ private readonly elementRef = inject(ElementRef);
private readonly gameStateService = inject(GameStateService);
private readonly window = inject(WINDOW_TOKEN);
@@ -36,6 +39,9 @@ export class AppComponent implements OnInit {
/** The size of the maze, defined as the number of rows/columns in the maze. */
size = 20;
+ /** Text to show in the snack bar. */
+ readonly snackBarText = signal(undefined);
+
ngOnInit(): void {
// Set dark mode if user's OS is using it.
if (
@@ -94,7 +100,8 @@ export class AppComponent implements OnInit {
shareMaze(): void {
const url = this.gameStateService.getShareUrl();
navigator.clipboard.writeText(url);
- alert('URL copied to clipboard'); // TODO: snackbar?
+ this.snackBarText.set('URL copied to clipboard');
+ setTimeout(() => this.closeSnackBar(), 2000);
}
/** Handles a move by the user. */
@@ -102,4 +109,15 @@ export class AppComponent implements OnInit {
if (this.gameStateService.inAnimation) return;
this.gameStateService.move(dir);
}
+
+ private closeSnackBar(): void {
+ // Set the opacity to 0 so the snack bar fades away before removing its text.
+ this.elementRef.nativeElement.querySelector('.snack-bar').style.opacity =
+ '0';
+ setTimeout(() => {
+ this.snackBarText.set(undefined);
+ this.elementRef.nativeElement.querySelector('.snack-bar').style.opacity =
+ '';
+ }, 1000);
+ }
}
diff --git a/src/app/game-state.service.spec.ts b/src/app/game-state.service.spec.ts
index c69971e..2e2e9a9 100644
--- a/src/app/game-state.service.spec.ts
+++ b/src/app/game-state.service.spec.ts
@@ -14,6 +14,10 @@ describe('GameStateService', () => {
clock.install();
});
+ afterAll(() => {
+ clock.uninstall();
+ });
+
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(GameStateService);
diff --git a/src/styles.scss b/src/styles.scss
index 526244d..16fb81f 100644
--- a/src/styles.scss
+++ b/src/styles.scss
@@ -1,4 +1,4 @@
-@import url('https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200');
+@import url('https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&family=Open+Sans:ital,wght@0,300..800;1,300..800');
* {
margin: 0;
@@ -20,6 +20,7 @@ body {
background: var(--background-color);
color: var(--text-color);
+ font-family: 'Open Sans', Arial, Helvetica, sans-serif;
&.dark-mode {
--background-color: #060606;