Toju Website V2

This commit is contained in:
2026-03-12 13:21:33 +01:00
parent 106212ef3d
commit 45e0b09af8
51 changed files with 2333 additions and 1513 deletions

View File

@@ -0,0 +1,9 @@
@if (adService.adsEnabled()) {
<div class="container mx-auto px-6 py-4">
<div
class="rounded-lg border border-dashed border-border/50 bg-card/30 min-h-[90px] flex items-center justify-center text-xs text-muted-foreground/50"
>
Advertisement
</div>
</div>
}

View File

@@ -4,15 +4,7 @@ import { AdService } from '../../services/ad.service';
@Component({
selector: 'app-ad-slot',
standalone: true,
template: `
@if (adService.adsEnabled()) {
<div class="container mx-auto px-6 py-4">
<div class="rounded-lg border border-dashed border-border/50 bg-card/30 min-h-[90px] flex items-center justify-center text-xs text-muted-foreground/50">
Advertisement
</div>
</div>
}
`
templateUrl: './ad-slot.component.html'
})
export class AdSlotComponent {
readonly adService = inject(AdService);

View File

@@ -0,0 +1,171 @@
<footer class="border-t border-border/30 bg-background/80 backdrop-blur-sm">
<div class="container mx-auto px-6 py-16">
<div class="grid grid-cols-1 md:grid-cols-4 gap-12">
<!-- Brand -->
<div class="md:col-span-1">
<div class="flex items-center gap-3 mb-4">
<img
src="/images/toju-logo-transparent.png"
alt="Toju"
class="h-8 w-auto object-contain"
/>
</div>
<p class="text-sm text-muted-foreground leading-relaxed">
Free, open-source, peer-to-peer communication. Built by people who believe privacy is a right, not a premium feature.
</p>
</div>
<!-- Product -->
<div>
<h4 class="text-sm font-semibold text-foreground mb-4 uppercase tracking-wider">Product</h4>
<ul class="space-y-3">
<li>
<a
routerLink="/downloads"
class="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
Downloads
</a>
</li>
<li>
<a
href="https://web.toju.app/"
target="_blank"
rel="noopener"
class="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
Web Version
</a>
</li>
<li>
<a
routerLink="/what-is-toju"
class="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
What is Toju?
</a>
</li>
<li>
<a
routerLink="/gallery"
class="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
Image Gallery
</a>
</li>
</ul>
</div>
<!-- Community -->
<div>
<h4 class="text-sm font-semibold text-foreground mb-4 uppercase tracking-wider">Community</h4>
<ul class="space-y-3">
<li>
<a
href="https://git.azaaxin.com/myxelium/Toju"
target="_blank"
rel="noopener"
class="inline-flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
>
<img
src="/images/gitea.png"
alt=""
width="16"
height="16"
class="w-4 h-4 object-contain"
/>
Source Code
</a>
</li>
<li>
<a
href="https://github.com/Myxelium"
target="_blank"
rel="noopener"
class="inline-flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
>
<img
src="/images/github.png"
alt=""
width="16"
height="16"
class="w-4 h-4 object-contain"
/>
GitHub
</a>
</li>
<li>
<a
href="https://buymeacoffee.com/myxelium"
target="_blank"
rel="noopener"
class="inline-flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
>
<img
src="/images/buymeacoffee.png"
alt=""
width="16"
height="16"
class="w-4 h-4 object-contain"
/>
Support Us
</a>
</li>
</ul>
</div>
<!-- Values -->
<div>
<h4 class="text-sm font-semibold text-foreground mb-4 uppercase tracking-wider">Values</h4>
<ul class="space-y-3">
<li>
<a
routerLink="/philosophy"
class="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
Our Philosophy
</a>
</li>
<li><span class="text-sm text-muted-foreground">100% Free Forever</span></li>
<li><span class="text-sm text-muted-foreground">Open Source</span></li>
</ul>
</div>
</div>
<div class="mt-12 pt-8 border-t border-border/30 flex flex-col md:flex-row justify-between items-center gap-4">
<p class="text-xs text-muted-foreground">&copy; {{ currentYear }} Myxelium. Toju is open-source software.</p>
<div class="flex items-center gap-4">
<a
href="https://git.azaaxin.com/myxelium/Toju"
target="_blank"
rel="noopener"
aria-label="View source code on Gitea"
class="text-muted-foreground hover:text-foreground transition-colors"
>
<img
src="/images/gitea.png"
alt=""
width="20"
height="20"
class="w-5 h-5 object-contain"
/>
</a>
<a
href="https://github.com/Myxelium"
target="_blank"
rel="noopener"
aria-label="View the project on GitHub"
class="text-muted-foreground hover:text-foreground transition-colors"
>
<img
src="/images/github.png"
alt=""
width="20"
height="20"
class="w-5 h-5 object-contain"
/>
</a>
</div>
</div>
</div>
</footer>

View File

@@ -5,65 +5,7 @@ import { RouterLink } from '@angular/router';
selector: 'app-footer',
standalone: true,
imports: [RouterLink],
template: `
<footer class="border-t border-border/30 bg-background/80 backdrop-blur-sm">
<div class="container mx-auto px-6 py-16">
<div class="grid grid-cols-1 md:grid-cols-4 gap-12">
<!-- Brand -->
<div class="md:col-span-1">
<div class="flex items-center gap-3 mb-4">
<div class="w-8 h-8 rounded-lg bg-gradient-to-br from-purple-500 to-violet-600 flex items-center justify-center font-bold text-white text-sm">T</div>
<span class="text-lg font-bold text-foreground">Toju</span>
</div>
<p class="text-sm text-muted-foreground leading-relaxed">
Free, open-source, peer-to-peer communication. Built by people who believe privacy is a right, not a premium feature.
</p>
</div>
<!-- Product -->
<div>
<h4 class="text-sm font-semibold text-foreground mb-4 uppercase tracking-wider">Product</h4>
<ul class="space-y-3">
<li><a routerLink="/downloads" class="text-sm text-muted-foreground hover:text-foreground transition-colors">Downloads</a></li>
<li><a href="https://web.toju.app/" target="_blank" rel="noopener" class="text-sm text-muted-foreground hover:text-foreground transition-colors">Web Version</a></li>
<li><a routerLink="/what-is-toju" class="text-sm text-muted-foreground hover:text-foreground transition-colors">What is Toju?</a></li>
</ul>
</div>
<!-- Community -->
<div>
<h4 class="text-sm font-semibold text-foreground mb-4 uppercase tracking-wider">Community</h4>
<ul class="space-y-3">
<li><a href="https://git.azaaxin.com/myxelium/Toju" target="_blank" rel="noopener" class="text-sm text-muted-foreground hover:text-foreground transition-colors">Source Code</a></li>
<li><a href="https://github.com/Myxelium" target="_blank" rel="noopener" class="text-sm text-muted-foreground hover:text-foreground transition-colors">GitHub</a></li>
<li><a href="https://buymeacoffee.com/myxelium" target="_blank" rel="noopener" class="text-sm text-muted-foreground hover:text-foreground transition-colors">Support Us</a></li>
</ul>
</div>
<!-- Values -->
<div>
<h4 class="text-sm font-semibold text-foreground mb-4 uppercase tracking-wider">Values</h4>
<ul class="space-y-3">
<li><a routerLink="/philosophy" class="text-sm text-muted-foreground hover:text-foreground transition-colors">Our Philosophy</a></li>
<li><span class="text-sm text-muted-foreground">100% Free Forever</span></li>
<li><span class="text-sm text-muted-foreground">Open Source</span></li>
</ul>
</div>
</div>
<div class="mt-12 pt-8 border-t border-border/30 flex flex-col md:flex-row justify-between items-center gap-4">
<p class="text-xs text-muted-foreground">
&copy; {{ currentYear }} Myxelium. Toju is open-source software.
</p>
<div class="flex items-center gap-4">
<a href="https://git.azaaxin.com/myxelium/Toju" target="_blank" rel="noopener" class="text-muted-foreground hover:text-foreground transition-colors">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>
</a>
</div>
</div>
</div>
</footer>
`
templateUrl: './footer.component.html'
})
export class FooterComponent {
readonly currentYear = new Date().getFullYear();

View File

@@ -0,0 +1,184 @@
<header
class="fixed top-0 left-0 right-0 z-50 transition-all duration-300"
[class]="scrolled() ? 'glass shadow-lg shadow-black/20' : 'bg-transparent'"
>
<nav class="container mx-auto px-6 py-4 flex items-center justify-between">
<!-- Logo -->
<a
routerLink="/"
aria-label="Toju home"
class="flex items-center group"
>
<span class="flex items-center gap-1.5">
<img
src="/images/toju-logo-transparent.png"
alt="Toju"
class="h-9 w-auto object-contain drop-shadow-lg group-hover:opacity-90 transition-opacity"
/>
<span class="text-xl font-bold text-foreground">Toju</span>
</span>
<span
class="ml-2 text-[10px] font-medium px-1.5 py-0.5 rounded-full bg-purple-500/20 text-purple-400 border border-purple-500/30 uppercase tracking-wider"
>
Beta
</span>
</a>
<!-- Desktop nav -->
<div class="hidden md:flex items-center gap-8">
<a
routerLink="/"
routerLinkActive="text-primary"
[routerLinkActiveOptions]="{ exact: true }"
class="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
Home
</a>
<a
routerLink="/what-is-toju"
routerLinkActive="text-primary"
class="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
What is Toju?
</a>
<a
routerLink="/downloads"
routerLinkActive="text-primary"
class="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
Downloads
</a>
<a
routerLink="/philosophy"
routerLinkActive="text-primary"
class="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
Our Philosophy
</a>
</div>
<!-- Right side -->
<div class="hidden md:flex items-center gap-4">
<a
href="https://buymeacoffee.com/myxelium"
target="_blank"
rel="noopener noreferrer"
class="text-sm text-muted-foreground hover:text-yellow-400 transition-colors flex items-center gap-1.5"
>
<img
src="/images/buymeacoffee.png"
alt=""
width="16"
height="16"
class="w-4 h-4 object-contain"
/>
Support Us
</a>
<a
href="https://web.toju.app/"
target="_blank"
rel="noopener"
class="inline-flex items-center gap-2 px-5 py-2 rounded-lg bg-gradient-to-r from-purple-600 to-violet-600 text-white text-sm font-medium hover:from-purple-500 hover:to-violet-500 transition-all shadow-lg shadow-purple-500/25 hover:shadow-purple-500/40"
>
Use Web Version
<svg
class="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
/>
</svg>
</a>
</div>
<!-- Mobile hamburger -->
<button
type="button"
class="md:hidden text-foreground p-2"
(click)="mobileOpen.set(!mobileOpen())"
aria-label="Toggle menu"
>
<svg
class="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
@if (mobileOpen()) {
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
} @else {
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"
/>
}
</svg>
</button>
</nav>
<!-- Mobile nav -->
@if (mobileOpen()) {
<div
class="md:hidden glass border-t border-border/30 px-6 py-4 space-y-4"
(click)="mobileOpen.set(false)"
>
<a
routerLink="/"
class="block text-sm text-muted-foreground hover:text-foreground transition-colors"
>Home</a
>
<a
routerLink="/what-is-toju"
class="block text-sm text-muted-foreground hover:text-foreground transition-colors"
>What is Toju?</a
>
<a
routerLink="/downloads"
class="block text-sm text-muted-foreground hover:text-foreground transition-colors"
>Downloads</a
>
<a
routerLink="/philosophy"
class="block text-sm text-muted-foreground hover:text-foreground transition-colors"
>Our Philosophy</a
>
<hr class="border-border/30" />
<a
href="https://buymeacoffee.com/myxelium"
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-2 text-sm text-muted-foreground hover:text-yellow-400 transition-colors"
>
<img
src="/images/buymeacoffee.png"
alt=""
width="16"
height="16"
class="w-4 h-4 object-contain"
/>
Support Us
</a>
<a
href="https://web.toju.app/"
target="_blank"
rel="noopener"
class="inline-flex items-center gap-2 px-5 py-2 rounded-lg bg-gradient-to-r from-purple-600 to-violet-600 text-white text-sm font-medium"
>
Use Web Version
</a>
</div>
}
</header>

View File

@@ -12,87 +12,14 @@ import { isPlatformBrowser } from '@angular/common';
selector: 'app-header',
standalone: true,
imports: [RouterLink, RouterLinkActive],
template: `
<header
class="fixed top-0 left-0 right-0 z-50 transition-all duration-300"
[class]="scrolled() ? 'glass shadow-lg shadow-black/20' : 'bg-transparent'"
>
<nav class="container mx-auto px-6 py-4 flex items-center justify-between">
<!-- Logo -->
<a routerLink="/" class="flex items-center gap-3 group">
<div class="w-9 h-9 rounded-lg bg-gradient-to-br from-purple-500 to-violet-600 flex items-center justify-center font-bold text-white text-lg shadow-lg shadow-purple-500/25 group-hover:shadow-purple-500/40 transition-shadow">
T
</div>
<span class="text-xl font-bold text-foreground">Toju</span>
<span class="text-[10px] font-medium px-1.5 py-0.5 rounded-full bg-purple-500/20 text-purple-400 border border-purple-500/30 uppercase tracking-wider">Beta</span>
</a>
<!-- Desktop nav -->
<div class="hidden md:flex items-center gap-8">
<a routerLink="/" routerLinkActive="text-primary" [routerLinkActiveOptions]="{exact: true}" class="text-sm text-muted-foreground hover:text-foreground transition-colors">Home</a>
<a routerLink="/what-is-toju" routerLinkActive="text-primary" class="text-sm text-muted-foreground hover:text-foreground transition-colors">What is Toju?</a>
<a routerLink="/downloads" routerLinkActive="text-primary" class="text-sm text-muted-foreground hover:text-foreground transition-colors">Downloads</a>
<a routerLink="/philosophy" routerLinkActive="text-primary" class="text-sm text-muted-foreground hover:text-foreground transition-colors">Our Philosophy</a>
</div>
<!-- Right side -->
<div class="hidden md:flex items-center gap-4">
<a
href="https://buymeacoffee.com/myxelium"
target="_blank"
rel="noopener noreferrer"
class="text-sm text-muted-foreground hover:text-yellow-400 transition-colors flex items-center gap-1.5"
>
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M20.216 6.415l-.132-.666c-.119-.598-.388-1.163-1.001-1.379-.197-.069-.42-.098-.57-.241-.152-.143-.196-.366-.231-.572-.065-.378-.125-.756-.192-1.133-.057-.325-.102-.69-.25-.987-.195-.4-.597-.634-.996-.788a5.723 5.723 0 00-.626-.194c-1-.263-2.05-.36-3.077-.416a25.834 25.834 0 00-3.7.062c-.915.083-1.88.184-2.75.5-.318.116-.646.256-.888.501-.297.302-.393.77-.177 1.146.154.267.415.456.692.58.36.162.737.284 1.123.366 1.075.238 2.189.331 3.287.37 1.218.05 2.437.01 3.65-.118.299-.033.598-.073.896-.119.352-.054.578-.513.474-.834-.124-.383-.457-.531-.834-.473-.466.074-.96.108-1.382.146-1.177.08-2.358.082-3.536.006a22.228 22.228 0 01-1.157-.107c-.086-.01-.18-.025-.258-.036-.243-.036-.484-.08-.724-.13-.111-.027-.111-.185 0-.212h.005c.277-.06.557-.108.838-.147h.002c.131-.009.263-.032.394-.048a25.076 25.076 0 013.426-.12c.674.019 1.347.062 2.014.13l.04.005c.394.04.781.098 1.172.166.063.011.121.029.182.044.088.03.18.043.252.087a.556.556 0 01.272.476c.014.09.042.217.068.343.082.397.162.79.24 1.184.048.26.092.52.122.782.017.163.047.414-.076.556-.163.178-.49.132-.702.096a48.146 48.146 0 00-1.553-.22 30.38 30.38 0 00-3.346-.234c-1.108 0-2.217.066-3.315.2a25.855 25.855 0 00-1.578.244c-.156.03-.313.062-.469.097-.288.065-.564.195-.754.441-.131.17-.18.383-.134.596.046.213.167.396.34.525.294.215.626.358.976.454 1.015.285 2.087.38 3.136.397 1.21.018 2.42-.04 3.618-.177 1.004-.114 2-.3 2.95-.593.082-.025.165-.05.246-.078.224-.073.458-.142.676-.233.378-.157.727-.397.856-.795a1.38 1.38 0 00.048-.353v-.015c0-.049-.01-.096-.014-.144l-.018-.121c-.028-.179-.055-.362-.1-.539l-.025-.115-.038-.186c-.022-.11-.044-.22-.07-.327l-.061-.283c-.02-.096-.042-.19-.067-.284-.02-.074-.037-.15-.058-.224a2.98 2.98 0 00-.09-.3c-.034-.095-.078-.225-.148-.308zm-11.8 6.56a.612.612 0 00-.35.177.614.614 0 00-.17.343l-.14.697-.147.726c-.012.065-.018.13-.026.194l-.13.653c-.012.065-.018.13-.026.195l-.13.652c-.024.119-.042.24-.07.358-.018.073-.037.147-.05.222l-.087.437c-.024.12-.042.24-.07.359-.018.073-.037.146-.05.221l-.088.437c-.011.058-.022.117-.03.176-.01.06-.014.119-.025.178a.474.474 0 00.147.422c.105.1.24.153.378.154l1.06.005c.037 0 .073-.003.11-.009l.053-.009c.151-.03.293-.109.364-.255.043-.09.058-.186.065-.282v-.003l.05-.259.052-.259.023-.117c.01-.051.019-.103.03-.153l.005-.025.026-.132.027-.131.026-.131.052-.259.052-.259.013-.064.013-.064.024-.12.024-.121c.012-.062.007-.127-.003-.189a.57.57 0 00-.044-.15.538.538 0 00-.094-.14.596.596 0 00-.23-.155c-.07-.032-.147-.052-.226-.057-.118-.008-.236-.008-.355-.008h-.007c-.154 0-.308 0-.462.003-.107.002-.216.002-.323.006zm13.7-2.974a.53.53 0 01.128.378c-.004.064-.02.128-.048.186a.684.684 0 01-.097.148l-.012.014-.016.016a.585.585 0 01-.13.098.612.612 0 01-.21.075 1.378 1.378 0 01-.26.024h-.037c-.086 0-.132.093-.094.161a.3.3 0 01.032.063c.018.045.018.093.009.14-.011.059-.048.11-.09.154a.332.332 0 01-.145.1.375.375 0 01-.173.03.505.505 0 01-.152-.036.43.43 0 01-.123-.082.456.456 0 01-.085-.11.435.435 0 01-.044-.138 1.01 1.01 0 01-.017-.134.66.66 0 01.005-.133c.008-.053.032-.1.065-.143.035-.044.078-.08.126-.107a.498.498 0 01.163-.059c.05-.007.1-.007.15-.002h.015a.08.08 0 00.076-.048.083.083 0 00-.023-.096.568.568 0 01-.123-.15.516.516 0 01-.056-.185.582.582 0 01.013-.196.592.592 0 01.075-.176.538.538 0 01.198-.182.567.567 0 01.247-.07c.08-.003.16.011.232.044a.56.56 0 01.187.14z"/></svg>
Support Us
</a>
<a
href="https://web.toju.app/"
target="_blank"
rel="noopener"
class="inline-flex items-center gap-2 px-5 py-2 rounded-lg bg-gradient-to-r from-purple-600 to-violet-600 text-white text-sm font-medium hover:from-purple-500 hover:to-violet-500 transition-all shadow-lg shadow-purple-500/25 hover:shadow-purple-500/40"
>
Use Web Version
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/></svg>
</a>
</div>
<!-- Mobile hamburger -->
<button
class="md:hidden text-foreground p-2"
(click)="mobileOpen.set(!mobileOpen())"
aria-label="Toggle menu"
>
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@if (mobileOpen()) {
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
} @else {
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
}
</svg>
</button>
</nav>
<!-- Mobile nav -->
@if (mobileOpen()) {
<div class="md:hidden glass border-t border-border/30 px-6 py-4 space-y-4" (click)="mobileOpen.set(false)">
<a routerLink="/" class="block text-sm text-muted-foreground hover:text-foreground transition-colors">Home</a>
<a routerLink="/what-is-toju" class="block text-sm text-muted-foreground hover:text-foreground transition-colors">What is Toju?</a>
<a routerLink="/downloads" class="block text-sm text-muted-foreground hover:text-foreground transition-colors">Downloads</a>
<a routerLink="/philosophy" class="block text-sm text-muted-foreground hover:text-foreground transition-colors">Our Philosophy</a>
<hr class="border-border/30">
<a href="https://buymeacoffee.com/myxelium" target="_blank" rel="noopener noreferrer" class="block text-sm text-muted-foreground hover:text-yellow-400 transition-colors">Support Us</a>
<a href="https://web.toju.app/" target="_blank" rel="noopener" class="inline-flex items-center gap-2 px-5 py-2 rounded-lg bg-gradient-to-r from-purple-600 to-violet-600 text-white text-sm font-medium">Use Web Version</a>
</div>
}
</header>
`
templateUrl: './header.component.html'
})
export class HeaderComponent {
private readonly platformId = inject(PLATFORM_ID);
readonly scrolled = signal(false);
readonly mobileOpen = signal(false);
private readonly platformId = inject(PLATFORM_ID);
@HostListener('window:scroll')
onScroll(): void {
if (isPlatformBrowser(this.platformId)) {

View File

@@ -1,41 +1,53 @@
import {
Component,
OnInit,
OnDestroy,
inject,
PLATFORM_ID,
ElementRef,
ViewChild
OnDestroy,
OnInit,
PLATFORM_ID,
ViewChild,
inject
} from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
@Component({
selector: 'app-particle-bg',
standalone: true,
template: '<canvas #canvas class="fixed inset-0 pointer-events-none z-0" style="pointer-events: all;"></canvas>',
styles: [':host { display: block; position: fixed; inset: 0; z-index: 0; pointer-events: none; } canvas { pointer-events: all; }']
host: {
class: 'block fixed inset-0 z-0 pointer-events-none'
},
template: '<canvas #canvas class="absolute inset-0 h-full w-full pointer-events-auto"></canvas>'
})
export class ParticleBgComponent implements OnInit, OnDestroy {
private readonly platformId = inject(PLATFORM_ID);
@ViewChild('canvas', { static: true }) canvasRef!: ElementRef<HTMLCanvasElement>;
@ViewChild('canvas', { static: true }) private canvasRef?: ElementRef<HTMLCanvasElement>;
private ctx!: CanvasRenderingContext2D;
private readonly platformId = inject(PLATFORM_ID);
private context: CanvasRenderingContext2D | null = null;
private particles: Particle[] = [];
private mouse = { x: -1000, y: -1000 };
private animId = 0;
private resizeHandler = () => this.resize();
private mouseMoveHandler = (e: MouseEvent) => {
this.mouse.x = e.clientX;
this.mouse.y = e.clientY;
private mousePosition = {
pointerX: -1000,
pointerY: -1000
};
private animationId = 0;
ngOnInit(): void {
if (!isPlatformBrowser(this.platformId))
if (!isPlatformBrowser(this.platformId)) {
return;
}
const canvas = this.canvasRef.nativeElement;
const canvas = this.canvasRef?.nativeElement;
this.ctx = canvas.getContext('2d')!;
if (!canvas) {
return;
}
const context = canvas.getContext('2d');
if (!context) {
return;
}
this.context = context;
this.resize();
window.addEventListener('resize', this.resizeHandler);
@@ -46,32 +58,46 @@ export class ParticleBgComponent implements OnInit, OnDestroy {
}
ngOnDestroy(): void {
if (!isPlatformBrowser(this.platformId))
if (!isPlatformBrowser(this.platformId)) {
return;
}
cancelAnimationFrame(this.animId);
cancelAnimationFrame(this.animationId);
window.removeEventListener('resize', this.resizeHandler);
window.removeEventListener('mousemove', this.mouseMoveHandler);
}
private readonly resizeHandler = () => this.resize();
private readonly mouseMoveHandler = (event: MouseEvent) => {
this.mousePosition.pointerX = event.clientX;
this.mousePosition.pointerY = event.clientY;
};
private resize(): void {
const canvas = this.canvasRef.nativeElement;
const canvas = this.canvasRef?.nativeElement;
if (!canvas) {
return;
}
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
private initParticles(): void {
const count = Math.min(80, Math.floor((window.innerWidth * window.innerHeight) / 15000));
const particleCount = Math.min(
80,
Math.floor((window.innerWidth * window.innerHeight) / 15000)
);
this.particles = [];
for (let i = 0; i < count; i++) {
for (let particleIndex = 0; particleIndex < particleCount; particleIndex++) {
this.particles.push({
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight,
vx: (Math.random() - 0.5) * 0.4,
vy: (Math.random() - 0.5) * 0.4,
positionX: Math.random() * window.innerWidth,
positionY: Math.random() * window.innerHeight,
velocityX: (Math.random() - 0.5) * 0.4,
velocityY: (Math.random() - 0.5) * 0.4,
radius: Math.random() * 2 + 0.5,
opacity: Math.random() * 0.5 + 0.1
});
@@ -79,99 +105,102 @@ export class ParticleBgComponent implements OnInit, OnDestroy {
}
private animate(): void {
const canvas = this.canvasRef.nativeElement;
const canvas = this.canvasRef?.nativeElement;
const context = this.context;
this.ctx.clearRect(0, 0, canvas.width, canvas.height);
for (const p of this.particles) {
// Mouse interaction - repel
const dx = p.x - this.mouse.x;
const dy = p.y - this.mouse.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 150) {
const force = (150 - dist) / 150;
p.vx += (dx / dist) * force * 0.3;
p.vy += (dy / dist) * force * 0.3;
}
// Dampen velocity
p.vx *= 0.98;
p.vy *= 0.98;
p.x += p.vx;
p.y += p.vy;
// Wrap
if (p.x < 0)
p.x = canvas.width;
if (p.x > canvas.width)
p.x = 0;
if (p.y < 0)
p.y = canvas.height;
if (p.y > canvas.height)
p.y = 0;
// Draw particle
this.ctx.beginPath();
this.ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
this.ctx.fillStyle = `rgba(139, 92, 246, ${p.opacity})`;
this.ctx.fill();
if (!canvas || !context) {
return;
}
// Draw connections
for (let i = 0; i < this.particles.length; i++) {
for (let j = i + 1; j < this.particles.length; j++) {
const a = this.particles[i];
const b = this.particles[j];
const dx = a.x - b.x;
const dy = a.y - b.y;
const dist = Math.sqrt(dx * dx + dy * dy);
context.clearRect(0, 0, canvas.width, canvas.height);
if (dist < 120) {
const opacity = (1 - dist / 120) * 0.15;
for (const particle of this.particles) {
const deltaX = particle.positionX - this.mousePosition.pointerX;
const deltaY = particle.positionY - this.mousePosition.pointerY;
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
this.ctx.beginPath();
this.ctx.moveTo(a.x, a.y);
this.ctx.lineTo(b.x, b.y);
this.ctx.strokeStyle = `rgba(139, 92, 246, ${opacity})`;
this.ctx.lineWidth = 0.5;
this.ctx.stroke();
if (distance < 150 && distance > 0) {
const force = (150 - distance) / 150;
particle.velocityX += (deltaX / distance) * force * 0.3;
particle.velocityY += (deltaY / distance) * force * 0.3;
}
particle.velocityX *= 0.98;
particle.velocityY *= 0.98;
particle.positionX += particle.velocityX;
particle.positionY += particle.velocityY;
if (particle.positionX < 0) {
particle.positionX = canvas.width;
}
if (particle.positionX > canvas.width) {
particle.positionX = 0;
}
if (particle.positionY < 0) {
particle.positionY = canvas.height;
}
if (particle.positionY > canvas.height) {
particle.positionY = 0;
}
context.beginPath();
context.arc(particle.positionX, particle.positionY, particle.radius, 0, Math.PI * 2);
context.fillStyle = `rgba(139, 92, 246, ${particle.opacity})`;
context.fill();
}
for (let particleIndex = 0; particleIndex < this.particles.length; particleIndex++) {
for (let connectionIndex = particleIndex + 1; connectionIndex < this.particles.length; connectionIndex++) {
const sourceParticle = this.particles[particleIndex];
const targetParticle = this.particles[connectionIndex];
const deltaX = sourceParticle.positionX - targetParticle.positionX;
const deltaY = sourceParticle.positionY - targetParticle.positionY;
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
if (distance < 120) {
const opacity = (1 - distance / 120) * 0.15;
context.beginPath();
context.moveTo(sourceParticle.positionX, sourceParticle.positionY);
context.lineTo(targetParticle.positionX, targetParticle.positionY);
context.strokeStyle = `rgba(139, 92, 246, ${opacity})`;
context.lineWidth = 0.5;
context.stroke();
}
}
}
// Draw connections to mouse
for (const p of this.particles) {
const dx = p.x - this.mouse.x;
const dy = p.y - this.mouse.y;
const dist = Math.sqrt(dx * dx + dy * dy);
for (const particle of this.particles) {
const deltaX = particle.positionX - this.mousePosition.pointerX;
const deltaY = particle.positionY - this.mousePosition.pointerY;
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
if (dist < 200) {
const opacity = (1 - dist / 200) * 0.25;
if (distance < 200) {
const opacity = (1 - distance / 200) * 0.25;
this.ctx.beginPath();
this.ctx.moveTo(p.x, p.y);
this.ctx.lineTo(this.mouse.x, this.mouse.y);
this.ctx.strokeStyle = `rgba(139, 92, 246, ${opacity})`;
this.ctx.lineWidth = 0.7;
this.ctx.stroke();
context.beginPath();
context.moveTo(particle.positionX, particle.positionY);
context.lineTo(this.mousePosition.pointerX, this.mousePosition.pointerY);
context.strokeStyle = `rgba(139, 92, 246, ${opacity})`;
context.lineWidth = 0.7;
context.stroke();
}
}
this.animId = requestAnimationFrame(() => this.animate());
this.animationId = requestAnimationFrame(() => this.animate());
}
}
interface Particle {
x: number;
y: number;
vx: number;
vy: number;
positionX: number;
positionY: number;
velocityX: number;
velocityY: number;
radius: number;
opacity: number;
}