diff --git a/web/projects/shared/assets/img/icons/saas/adobe.svg b/web/projects/shared/assets/img/icons/saas/adobe.svg
new file mode 100644
index 000000000..2f4374c8a
--- /dev/null
+++ b/web/projects/shared/assets/img/icons/saas/adobe.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/projects/shared/assets/img/icons/saas/amazon.svg b/web/projects/shared/assets/img/icons/saas/amazon.svg
new file mode 100644
index 000000000..878e92e68
--- /dev/null
+++ b/web/projects/shared/assets/img/icons/saas/amazon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/projects/shared/assets/img/icons/saas/anthropic.svg b/web/projects/shared/assets/img/icons/saas/anthropic.svg
new file mode 100644
index 000000000..d7e3b44f1
--- /dev/null
+++ b/web/projects/shared/assets/img/icons/saas/anthropic.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/projects/shared/assets/img/icons/saas/apple.svg b/web/projects/shared/assets/img/icons/saas/apple.svg
new file mode 100644
index 000000000..cab0d6c61
--- /dev/null
+++ b/web/projects/shared/assets/img/icons/saas/apple.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/projects/shared/assets/img/icons/saas/atlassian.svg b/web/projects/shared/assets/img/icons/saas/atlassian.svg
new file mode 100644
index 000000000..6eaac6cf8
--- /dev/null
+++ b/web/projects/shared/assets/img/icons/saas/atlassian.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/projects/shared/assets/img/icons/saas/box.svg b/web/projects/shared/assets/img/icons/saas/box.svg
new file mode 100644
index 000000000..f0c83f2c6
--- /dev/null
+++ b/web/projects/shared/assets/img/icons/saas/box.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/projects/shared/assets/img/icons/saas/cloudflare.svg b/web/projects/shared/assets/img/icons/saas/cloudflare.svg
new file mode 100644
index 000000000..66cc02086
--- /dev/null
+++ b/web/projects/shared/assets/img/icons/saas/cloudflare.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/projects/shared/assets/img/icons/saas/datadog.svg b/web/projects/shared/assets/img/icons/saas/datadog.svg
new file mode 100644
index 000000000..ab4373190
--- /dev/null
+++ b/web/projects/shared/assets/img/icons/saas/datadog.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/projects/shared/assets/img/icons/saas/discord.svg b/web/projects/shared/assets/img/icons/saas/discord.svg
new file mode 100644
index 000000000..ef25142a3
--- /dev/null
+++ b/web/projects/shared/assets/img/icons/saas/discord.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/projects/shared/assets/img/icons/saas/dropbox.svg b/web/projects/shared/assets/img/icons/saas/dropbox.svg
new file mode 100644
index 000000000..2fc268685
--- /dev/null
+++ b/web/projects/shared/assets/img/icons/saas/dropbox.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/projects/shared/assets/img/icons/saas/github.svg b/web/projects/shared/assets/img/icons/saas/github.svg
new file mode 100644
index 000000000..4532de0e8
--- /dev/null
+++ b/web/projects/shared/assets/img/icons/saas/github.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/projects/shared/assets/img/icons/saas/gitlab.svg b/web/projects/shared/assets/img/icons/saas/gitlab.svg
new file mode 100644
index 000000000..2910f4aa4
--- /dev/null
+++ b/web/projects/shared/assets/img/icons/saas/gitlab.svg
@@ -0,0 +1 @@
+
diff --git a/web/projects/shared/assets/img/icons/saas/godaddy.svg b/web/projects/shared/assets/img/icons/saas/godaddy.svg
new file mode 100644
index 000000000..2c95d1642
--- /dev/null
+++ b/web/projects/shared/assets/img/icons/saas/godaddy.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/projects/shared/assets/img/icons/saas/google.svg b/web/projects/shared/assets/img/icons/saas/google.svg
new file mode 100644
index 000000000..354fb4abc
--- /dev/null
+++ b/web/projects/shared/assets/img/icons/saas/google.svg
@@ -0,0 +1 @@
+
diff --git a/web/projects/shared/assets/img/icons/saas/hubspot.svg b/web/projects/shared/assets/img/icons/saas/hubspot.svg
new file mode 100644
index 000000000..1b9ddd2be
--- /dev/null
+++ b/web/projects/shared/assets/img/icons/saas/hubspot.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/projects/shared/assets/img/icons/saas/icloud.svg b/web/projects/shared/assets/img/icons/saas/icloud.svg
new file mode 100644
index 000000000..bf54590c0
--- /dev/null
+++ b/web/projects/shared/assets/img/icons/saas/icloud.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/projects/shared/assets/img/icons/saas/lastpass.svg b/web/projects/shared/assets/img/icons/saas/lastpass.svg
new file mode 100644
index 000000000..50ccd7bf5
--- /dev/null
+++ b/web/projects/shared/assets/img/icons/saas/lastpass.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/projects/shared/assets/img/icons/saas/meta.svg b/web/projects/shared/assets/img/icons/saas/meta.svg
new file mode 100644
index 000000000..af72710b2
--- /dev/null
+++ b/web/projects/shared/assets/img/icons/saas/meta.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/projects/shared/assets/img/icons/saas/microsoft.svg b/web/projects/shared/assets/img/icons/saas/microsoft.svg
new file mode 100644
index 000000000..c7b53ae83
--- /dev/null
+++ b/web/projects/shared/assets/img/icons/saas/microsoft.svg
@@ -0,0 +1 @@
+
diff --git a/web/projects/shared/assets/img/icons/saas/mongodb.svg b/web/projects/shared/assets/img/icons/saas/mongodb.svg
new file mode 100644
index 000000000..13d7d00a6
--- /dev/null
+++ b/web/projects/shared/assets/img/icons/saas/mongodb.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/projects/shared/assets/img/icons/saas/netflix.svg b/web/projects/shared/assets/img/icons/saas/netflix.svg
new file mode 100644
index 000000000..385426608
--- /dev/null
+++ b/web/projects/shared/assets/img/icons/saas/netflix.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/projects/shared/assets/img/icons/saas/notion.svg b/web/projects/shared/assets/img/icons/saas/notion.svg
new file mode 100644
index 000000000..e50096a05
--- /dev/null
+++ b/web/projects/shared/assets/img/icons/saas/notion.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/projects/shared/assets/img/icons/saas/onepassword.svg b/web/projects/shared/assets/img/icons/saas/onepassword.svg
new file mode 100644
index 000000000..602256d72
--- /dev/null
+++ b/web/projects/shared/assets/img/icons/saas/onepassword.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/projects/shared/assets/img/icons/saas/openai.svg b/web/projects/shared/assets/img/icons/saas/openai.svg
new file mode 100644
index 000000000..cb1897b63
--- /dev/null
+++ b/web/projects/shared/assets/img/icons/saas/openai.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/projects/shared/assets/img/icons/saas/paypal.svg b/web/projects/shared/assets/img/icons/saas/paypal.svg
new file mode 100644
index 000000000..840b18910
--- /dev/null
+++ b/web/projects/shared/assets/img/icons/saas/paypal.svg
@@ -0,0 +1 @@
+
diff --git a/web/projects/shared/assets/img/icons/saas/salesforce.svg b/web/projects/shared/assets/img/icons/saas/salesforce.svg
new file mode 100644
index 000000000..a5b6cc7ac
--- /dev/null
+++ b/web/projects/shared/assets/img/icons/saas/salesforce.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/projects/shared/assets/img/icons/saas/shopify.svg b/web/projects/shared/assets/img/icons/saas/shopify.svg
new file mode 100644
index 000000000..525bd2b93
--- /dev/null
+++ b/web/projects/shared/assets/img/icons/saas/shopify.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/projects/shared/assets/img/icons/saas/slack.svg b/web/projects/shared/assets/img/icons/saas/slack.svg
new file mode 100644
index 000000000..a41e53bab
--- /dev/null
+++ b/web/projects/shared/assets/img/icons/saas/slack.svg
@@ -0,0 +1 @@
+
diff --git a/web/projects/shared/assets/img/icons/saas/spotify.svg b/web/projects/shared/assets/img/icons/saas/spotify.svg
new file mode 100644
index 000000000..c3ad6562a
--- /dev/null
+++ b/web/projects/shared/assets/img/icons/saas/spotify.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/projects/shared/assets/img/icons/saas/square.svg b/web/projects/shared/assets/img/icons/saas/square.svg
new file mode 100644
index 000000000..96a69d079
--- /dev/null
+++ b/web/projects/shared/assets/img/icons/saas/square.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/projects/shared/assets/img/icons/saas/squarespace.svg b/web/projects/shared/assets/img/icons/saas/squarespace.svg
new file mode 100644
index 000000000..23946ab1c
--- /dev/null
+++ b/web/projects/shared/assets/img/icons/saas/squarespace.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/projects/shared/assets/img/icons/saas/stripe.svg b/web/projects/shared/assets/img/icons/saas/stripe.svg
new file mode 100644
index 000000000..48272a2c5
--- /dev/null
+++ b/web/projects/shared/assets/img/icons/saas/stripe.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/projects/shared/assets/img/icons/saas/twilio.svg b/web/projects/shared/assets/img/icons/saas/twilio.svg
new file mode 100644
index 000000000..2392f4bd9
--- /dev/null
+++ b/web/projects/shared/assets/img/icons/saas/twilio.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/projects/shared/assets/img/icons/saas/wix.svg b/web/projects/shared/assets/img/icons/saas/wix.svg
new file mode 100644
index 000000000..cd1480dd9
--- /dev/null
+++ b/web/projects/shared/assets/img/icons/saas/wix.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/projects/shared/assets/img/icons/saas/zoom.svg b/web/projects/shared/assets/img/icons/saas/zoom.svg
new file mode 100644
index 000000000..28828a365
--- /dev/null
+++ b/web/projects/shared/assets/img/icons/saas/zoom.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/projects/shared/assets/img/icons/snek.png b/web/projects/shared/assets/img/icons/snake.png
similarity index 100%
rename from web/projects/shared/assets/img/icons/snek.png
rename to web/projects/shared/assets/img/icons/snake.png
diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/general/general.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/general/general.component.ts
index e9509a2fc..dc0a79110 100644
--- a/web/projects/ui/src/app/routes/portal/routes/system/routes/general/general.component.ts
+++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/general/general.component.ts
@@ -49,7 +49,7 @@ import { OSService } from 'src/app/services/os.service'
import { DataModel } from 'src/app/services/patch-db/data-model'
import { TitleDirective } from 'src/app/services/title.service'
import { ABOUT } from 'src/app/routes/portal/components/header/about.component'
-import { SnekDirective } from './snek.directive'
+import { SnakeDirective } from './snake.directive'
import { UPDATE } from './update.component'
import { KeyboardSelectComponent } from './keyboard-select.component'
import { ServerNameDialog } from './server-name.dialog'
@@ -203,10 +203,10 @@ import { ServerNameDialog } from './server-name.dialog'
}
}
`,
@@ -215,7 +215,7 @@ import { ServerNameDialog } from './server-name.dialog'
max-inline-size: 40rem;
}
- .snek {
+ .snake {
width: 1rem;
opacity: 0.2;
cursor: pointer;
@@ -270,7 +270,7 @@ import { ServerNameDialog } from './server-name.dialog'
TuiDataListWrapper,
TuiTextfield,
FormsModule,
- SnekDirective,
+ SnakeDirective,
TuiBadge,
TuiBadgeNotification,
TuiAnimated,
diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/general/snek.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/general/snake.component.ts
similarity index 83%
rename from web/projects/ui/src/app/routes/portal/routes/system/routes/general/snek.component.ts
rename to web/projects/ui/src/app/routes/portal/routes/system/routes/general/snake.component.ts
index ebc947a67..f820911c5 100644
--- a/web/projects/ui/src/app/routes/portal/routes/system/routes/general/snek.component.ts
+++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/general/snake.component.ts
@@ -36,6 +36,44 @@ const GRID_H = 26
const SPEED = 45
const STARTING_LENGTH = 4
+const SAAS_ICONS = [
+ 'adobe',
+ 'amazon',
+ 'anthropic',
+ 'apple',
+ 'atlassian',
+ 'box',
+ 'cloudflare',
+ 'datadog',
+ 'discord',
+ 'dropbox',
+ 'github',
+ 'gitlab',
+ 'godaddy',
+ 'google',
+ 'hubspot',
+ 'icloud',
+ 'lastpass',
+ 'meta',
+ 'microsoft',
+ 'mongodb',
+ 'netflix',
+ 'notion',
+ 'onepassword',
+ 'openai',
+ 'paypal',
+ 'salesforce',
+ 'shopify',
+ 'slack',
+ 'spotify',
+ 'squarespace',
+ 'square',
+ 'stripe',
+ 'twilio',
+ 'wix',
+ 'zoom',
+].map(name => `assets/img/icons/saas/${name}.svg`)
+
function lerpColor(from: RGB, to: RGB, t: number): string {
const r = Math.round(from[0] + (to[0] - from[0]) * t)
const g = Math.round(from[1] + (to[1] - from[1]) * t)
@@ -128,7 +166,7 @@ function lerpColor(from: RGB, to: RGB, t: number): string {
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [TuiButton, i18nPipe],
})
-export class SnekComponent {
+export class SnakeComponent {
private readonly destroyRef = inject(DestroyRef)
private readonly dialog = injectContext>()
private readonly canvasRef = viewChild>('game')
@@ -139,27 +177,30 @@ export class SnekComponent {
score = 0
private grid = NaN
+ private canvasW = 0
+ private canvasH = 0
private ctx!: CanvasRenderingContext2D
- private image = new Image()
- private imageLoaded = false
+ private images: HTMLImageElement[] = []
+ private currentImage: HTMLImageElement | null = null
private animationId = 0
private lastTime = 0
private dead = false
private snake!: Snake
- private bitcoin: Point = { x: NaN, y: NaN }
+ private food: Point = { x: NaN, y: NaN }
private moveQueue: string[] = []
constructor() {
- this.image.onload = () => {
- this.imageLoaded = true
+ for (const src of SAAS_ICONS) {
+ const img = new Image()
+ img.src = src
+ this.images.push(img)
}
- this.image.src = 'assets/img/icons/bitcoin.svg'
afterNextRender(() => {
this.initCanvas()
this.snake = this.createSnake()
- this.spawnBitcoin()
+ this.spawnFood()
this.drawFrame()
this.animationId = requestAnimationFrame(t => this.loop(t))
})
@@ -259,6 +300,7 @@ export class SnekComponent {
this.ctx = canvas.getContext('2d')!
const container = canvas.parentElement!
+ const dpr = window.devicePixelRatio || 1
// Size grid based on available width, cap so canvas height stays reasonable
const maxHeight = window.innerHeight * 0.55
@@ -267,8 +309,14 @@ export class SnekComponent {
Math.floor(maxHeight / GRID_H),
)
- canvas.width = this.grid * GRID_W
- canvas.height = this.grid * GRID_H
+ this.canvasW = this.grid * GRID_W
+ this.canvasH = this.grid * GRID_H
+
+ canvas.width = this.canvasW * dpr
+ canvas.height = this.canvasH * dpr
+ canvas.style.width = `${this.canvasW}px`
+ canvas.style.height = `${this.canvasH}px`
+ this.ctx.scale(dpr, dpr)
}
private createSnake(): Snake {
@@ -288,18 +336,28 @@ export class SnekComponent {
return this.grid * Math.floor(GRID_H / 2)
}
- private spawnBitcoin() {
- this.bitcoin = {
+ private spawnFood() {
+ this.food = {
x: this.randomInt(0, GRID_W) * this.grid,
y: this.randomInt(0, GRID_H) * this.grid,
}
+
+ const img = this.images[this.randomInt(0, this.images.length)]!
+ this.currentImage = img.complete && img.naturalWidth ? img : null
+
+ if (!this.currentImage) {
+ img.onload = () => {
+ this.currentImage = img
+ this.drawFrame()
+ }
+ }
}
private restart() {
this.score = 0
this.snake = this.createSnake()
this.moveQueue = []
- this.spawnBitcoin()
+ this.spawnFood()
this.lastTime = 0
this.state.set('playing')
}
@@ -351,15 +409,12 @@ export class SnekComponent {
this.snake.cells.pop()
}
- const canvas = this.canvasRef()?.nativeElement
- if (!canvas) return
-
// Wall collision
if (
newHead.x < 0 ||
newHead.y < 0 ||
- newHead.x >= canvas.width ||
- newHead.y >= canvas.height
+ newHead.x >= this.canvasW ||
+ newHead.y >= this.canvasH
) {
this.onDeath()
return
@@ -374,12 +429,12 @@ export class SnekComponent {
}
}
- // Eat bitcoin
- if (newHead.x === this.bitcoin.x && newHead.y === this.bitcoin.y) {
+ // Eat food
+ if (newHead.x === this.food.x && newHead.y === this.food.y) {
this.score++
this.highScore = Math.max(this.score, this.highScore)
this.snake.maxCells++
- this.spawnBitcoin()
+ this.spawnFood()
}
}
@@ -393,20 +448,19 @@ export class SnekComponent {
}
private drawFrame() {
- const canvas = this.canvasRef()?.nativeElement
- if (!canvas || !this.ctx) return
+ if (!this.ctx) return
- this.ctx.clearRect(0, 0, canvas.width, canvas.height)
- this.drawBitcoin()
+ this.ctx.clearRect(0, 0, this.canvasW, this.canvasH)
+ this.drawFood()
this.drawSnake()
}
- private drawBitcoin() {
- if (!this.imageLoaded) return
+ private drawFood() {
+ if (!this.currentImage) return
this.ctx.drawImage(
- this.image,
- this.bitcoin.x,
- this.bitcoin.y,
+ this.currentImage,
+ this.food.x,
+ this.food.y,
this.grid,
this.grid,
)
@@ -417,8 +471,7 @@ export class SnekComponent {
if (cells.length === 0) {
// Draw initial position in bottom-left corner (out of overlay text)
const x = STARTING_LENGTH * this.grid
- const canvas = this.canvasRef()?.nativeElement
- const y = canvas ? canvas.height - this.grid * 2 : this.getStartY()
+ const y = this.canvasH ? this.canvasH - this.grid * 2 : this.getStartY()
for (let i = 0; i < STARTING_LENGTH; i++) {
const t = STARTING_LENGTH > 1 ? i / (STARTING_LENGTH - 1) : 0
this.ctx.fillStyle = lerpColor(HEAD_COLOR, TAIL_COLOR, t)
diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/general/snek.directive.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/general/snake.directive.ts
similarity index 80%
rename from web/projects/ui/src/app/routes/portal/routes/system/routes/general/snek.directive.ts
rename to web/projects/ui/src/app/routes/portal/routes/system/routes/general/snake.directive.ts
index eff791c83..470dcd4da 100644
--- a/web/projects/ui/src/app/routes/portal/routes/system/routes/general/snek.directive.ts
+++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/general/snake.directive.ts
@@ -8,31 +8,31 @@ import {
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
import { filter } from 'rxjs'
import { ApiService } from 'src/app/services/api/embassy-api.service'
-import { SnekComponent } from './snek.component'
+import { SnakeComponent } from './snake.component'
@Directive({
- selector: 'img[snek]',
+ selector: 'img[snake]',
})
-export class SnekDirective {
+export class SnakeDirective {
private readonly loader = inject(LoadingService)
private readonly errorService = inject(ErrorService)
private readonly api = inject(ApiService)
private readonly dialog = inject(DialogService)
@Input()
- snek = 0
+ snake = 0
@HostListener('click')
async onClick() {
this.dialog
- .openComponent(new PolymorpheusComponent(SnekComponent), {
+ .openComponent(new PolymorpheusComponent(SnakeComponent), {
label: 'Snake!' as i18nKey,
size: 'l',
closeable: false,
dismissible: false,
- data: this.snek,
+ data: this.snake,
})
- .pipe(filter(score => score > this.snek))
+ .pipe(filter(score => score > this.snake))
.subscribe(async score => {
const loader = this.loader.open('Saving high score').subscribe()