From b168a9e3c1b26f34a254b841c1aa1184dd0be61d Mon Sep 17 00:00:00 2001
From: Ayush Jain <136119112+URAYUSHJAIN@users.noreply.github.com>
Date: Sat, 11 Oct 2025 14:19:05 +0530
Subject: [PATCH] Feature/create infinite logo carousel component (#5671)
* Create infinite logo carousel component
* Update changelog
---
CHANGELOG.md | 1 +
.../pages/landing/landing-page.component.ts | 2 +
.../src/app/pages/landing/landing-page.html | 105 +--------
.../src/app/pages/landing/landing-page.scss | 76 +------
libs/ui/src/lib/logo-carousel/index.ts | 1 +
.../logo-carousel/interfaces/interfaces.ts | 7 +
.../logo-carousel.component.html | 16 ++
.../logo-carousel.component.scss | 213 ++++++++++++++++++
.../logo-carousel.component.stories.ts | 13 ++
.../logo-carousel/logo-carousel.component.ts | 110 +++++++++
10 files changed, 366 insertions(+), 178 deletions(-)
create mode 100644 libs/ui/src/lib/logo-carousel/index.ts
create mode 100644 libs/ui/src/lib/logo-carousel/interfaces/interfaces.ts
create mode 100644 libs/ui/src/lib/logo-carousel/logo-carousel.component.html
create mode 100644 libs/ui/src/lib/logo-carousel/logo-carousel.component.scss
create mode 100644 libs/ui/src/lib/logo-carousel/logo-carousel.component.stories.ts
create mode 100644 libs/ui/src/lib/logo-carousel/logo-carousel.component.ts
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 197bcdac5..544a7bdc6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
+- Changed the _As seen in_ section on the landing page to an animated carousel
- Refactored various components to use self-closing tags
### Fixed
diff --git a/apps/client/src/app/pages/landing/landing-page.component.ts b/apps/client/src/app/pages/landing/landing-page.component.ts
index f16a9c14a..f755494e0 100644
--- a/apps/client/src/app/pages/landing/landing-page.component.ts
+++ b/apps/client/src/app/pages/landing/landing-page.component.ts
@@ -4,6 +4,7 @@ import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { publicRoutes } from '@ghostfolio/common/routes/routes';
import { GfCarouselComponent } from '@ghostfolio/ui/carousel';
import { GfLogoComponent } from '@ghostfolio/ui/logo';
+import { GfLogoCarouselComponent } from '@ghostfolio/ui/logo-carousel';
import { GfValueComponent } from '@ghostfolio/ui/value';
import { GfWorldMapChartComponent } from '@ghostfolio/ui/world-map-chart';
@@ -27,6 +28,7 @@ import { Subject } from 'rxjs';
imports: [
CommonModule,
GfCarouselComponent,
+ GfLogoCarouselComponent,
GfLogoComponent,
GfValueComponent,
GfWorldMapChartComponent,
diff --git a/apps/client/src/app/pages/landing/landing-page.html b/apps/client/src/app/pages/landing/landing-page.html
index 7202e2910..7f77c31a7 100644
--- a/apps/client/src/app/pages/landing/landing-page.html
+++ b/apps/client/src/app/pages/landing/landing-page.html
@@ -114,109 +114,8 @@
As seen in
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/apps/client/src/app/pages/landing/landing-page.scss b/apps/client/src/app/pages/landing/landing-page.scss
index 4c0c14efd..35f362b0f 100644
--- a/apps/client/src/app/pages/landing/landing-page.scss
+++ b/apps/client/src/app/pages/landing/landing-page.scss
@@ -34,69 +34,6 @@
&.logo-agplv3 {
mask-image: url('/assets/images/logo-AGPLv3.svg');
}
-
- &.logo-alternative-to {
- mask-image: url('/assets/images/logo-alternative-to.svg');
- }
-
- &.logo-awesome {
- background-image: url('/assets/images/logo-awesome.png');
- background-position: center;
- background-repeat: no-repeat;
- background-size: contain;
- filter: grayscale(1);
- }
-
- &.logo-dev-community {
- mask-image: url('/assets/images/logo-dev-community.svg');
- }
-
- &.logo-hacker-news {
- mask-image: url('/assets/images/logo-hacker-news.svg');
- }
-
- &.logo-openalternative {
- mask-image: url('/assets/images/logo-openalternative.svg');
- }
-
- &.logo-privacy-tools {
- mask-image: url('/assets/images/logo-privacy-tools.svg');
- }
-
- &.logo-product-hunt {
- background-image: url('/assets/images/logo-product-hunt.png');
- background-position: center;
- background-repeat: no-repeat;
- background-size: contain;
- filter: grayscale(1);
- }
-
- &.logo-reddit {
- mask-image: url('/assets/images/logo-reddit.svg');
- max-height: 1rem;
- }
-
- &.logo-sackgeld {
- mask-image: url('/assets/images/logo-sackgeld.png');
- }
-
- &.logo-selfh-st {
- mask-image: url('/assets/images/logo-selfh-st.svg');
- max-height: 1.25rem;
- }
-
- &.logo-sourceforge {
- mask-image: url('/assets/images/logo-sourceforge.svg');
- }
-
- &.logo-umbrel {
- mask-image: url('/assets/images/logo-umbrel.svg');
- max-height: 1.5rem;
- }
-
- &.logo-unraid {
- mask-image: url('/assets/images/logo-unraid.svg');
- }
}
.outro-inner-container {
@@ -128,18 +65,7 @@
}
.logo {
- &.logo-agplv3,
- &.logo-alternative-to,
- &.logo-dev-community,
- &.logo-hacker-news,
- &.logo-openalternative,
- &.logo-privacy-tools,
- &.logo-reddit,
- &.logo-sackgeld,
- &.logo-selfh-st,
- &.logo-sourceforge,
- &.logo-umbrel,
- &.logo-unraid {
+ &.logo-agplv3 {
background-color: rgba(var(--light-primary-text));
}
}
diff --git a/libs/ui/src/lib/logo-carousel/index.ts b/libs/ui/src/lib/logo-carousel/index.ts
new file mode 100644
index 000000000..e69f6039c
--- /dev/null
+++ b/libs/ui/src/lib/logo-carousel/index.ts
@@ -0,0 +1 @@
+export * from './logo-carousel.component';
diff --git a/libs/ui/src/lib/logo-carousel/interfaces/interfaces.ts b/libs/ui/src/lib/logo-carousel/interfaces/interfaces.ts
new file mode 100644
index 000000000..43dcfeefd
--- /dev/null
+++ b/libs/ui/src/lib/logo-carousel/interfaces/interfaces.ts
@@ -0,0 +1,7 @@
+export interface LogoItem {
+ className: string;
+ isMask?: boolean;
+ name: string;
+ title: string;
+ url: string;
+}
diff --git a/libs/ui/src/lib/logo-carousel/logo-carousel.component.html b/libs/ui/src/lib/logo-carousel/logo-carousel.component.html
new file mode 100644
index 000000000..79b4ca11b
--- /dev/null
+++ b/libs/ui/src/lib/logo-carousel/logo-carousel.component.html
@@ -0,0 +1,16 @@
+
+
+ @for (logo of logosRepeated; track $index) {
+
+ }
+
+
diff --git a/libs/ui/src/lib/logo-carousel/logo-carousel.component.scss b/libs/ui/src/lib/logo-carousel/logo-carousel.component.scss
new file mode 100644
index 000000000..d8a8865f7
--- /dev/null
+++ b/libs/ui/src/lib/logo-carousel/logo-carousel.component.scss
@@ -0,0 +1,213 @@
+:host {
+ display: block;
+ overflow: hidden;
+ position: relative;
+ width: 100%;
+
+ .logo-carousel-container {
+ &::before,
+ &::after {
+ content: '';
+ height: 100%;
+ pointer-events: none;
+ position: absolute;
+ top: 0;
+ width: 100px;
+ z-index: 2;
+ }
+
+ &::before {
+ background: linear-gradient(
+ to right,
+ var(--light-background) 0%,
+ rgba(var(--palette-background-background), 0) 100%
+ );
+ left: 0;
+ }
+
+ &::after {
+ background: linear-gradient(
+ to left,
+ var(--light-background) 0%,
+ rgba(var(--palette-background-background), 0) 100%
+ );
+ right: 0;
+ }
+
+ @media (max-width: 768px) {
+ &::before,
+ &::after {
+ width: 50px;
+ }
+ }
+
+ @media (max-width: 576px) {
+ &::before,
+ &::after {
+ width: 30px;
+ }
+ }
+
+ .logo-carousel-track {
+ animation: scroll 60s linear infinite;
+ width: fit-content;
+
+ &:hover {
+ animation-play-state: paused;
+ }
+
+ .logo-carousel-item {
+ flex-shrink: 0;
+ min-width: 200px;
+ padding: 0 2rem;
+
+ @media (max-width: 768px) {
+ min-width: 150px;
+ padding: 0 1.5rem;
+ }
+
+ @media (max-width: 576px) {
+ min-width: 120px;
+ padding: 0 1rem;
+ }
+
+ .logo {
+ height: 3rem;
+ transition:
+ opacity 0.3s ease,
+ transform 0.3s ease;
+ width: 7.5rem;
+
+ &:hover {
+ opacity: 0.8;
+ }
+
+ &.mask {
+ background-color: rgba(var(--dark-secondary-text));
+ mask-position: center;
+ mask-repeat: no-repeat;
+ mask-size: contain;
+ }
+
+ &.logo-alternative-to {
+ mask-image: url('/assets/images/logo-alternative-to.svg');
+ }
+
+ &.logo-awesome {
+ background-image: url('/assets/images/logo-awesome.png');
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: contain;
+ filter: grayscale(1);
+ }
+
+ &.logo-dev-community {
+ mask-image: url('/assets/images/logo-dev-community.svg');
+ }
+
+ &.logo-hacker-news {
+ mask-image: url('/assets/images/logo-hacker-news.svg');
+ }
+
+ &.logo-openalternative {
+ mask-image: url('/assets/images/logo-openalternative.svg');
+ }
+
+ &.logo-privacy-tools {
+ mask-image: url('/assets/images/logo-privacy-tools.svg');
+ }
+
+ &.logo-product-hunt {
+ background-image: url('/assets/images/logo-product-hunt.png');
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: contain;
+ filter: grayscale(1);
+ }
+
+ &.logo-reddit {
+ mask-image: url('/assets/images/logo-reddit.svg');
+ max-height: 1rem;
+ }
+
+ &.logo-sackgeld {
+ mask-image: url('/assets/images/logo-sackgeld.png');
+ }
+
+ &.logo-selfh-st {
+ mask-image: url('/assets/images/logo-selfh-st.svg');
+ max-height: 1.25rem;
+ }
+
+ &.logo-sourceforge {
+ mask-image: url('/assets/images/logo-sourceforge.svg');
+ }
+
+ &.logo-umbrel {
+ mask-image: url('/assets/images/logo-umbrel.svg');
+ max-height: 1.5rem;
+ }
+
+ &.logo-unraid {
+ mask-image: url('/assets/images/logo-unraid.svg');
+ }
+
+ @media (max-width: 768px) {
+ height: 2.5rem;
+ width: 6rem;
+ }
+
+ @media (max-width: 576px) {
+ height: 2rem;
+ width: 5rem;
+ }
+ }
+ }
+
+ @keyframes scroll {
+ 0% {
+ transform: translateX(0);
+ }
+ 100% {
+ transform: translateX(-50%);
+ }
+ }
+ }
+ }
+}
+
+:host-context(.theme-dark) {
+ .logo-carousel-container {
+ &::before {
+ background: linear-gradient(
+ to right,
+ var(--dark-background) 0%,
+ rgba(var(--palette-background-background-dark), 0) 100%
+ );
+ }
+
+ &::after {
+ background: linear-gradient(
+ to left,
+ var(--dark-background) 0%,
+ rgba(var(--palette-background-background-dark), 0) 100%
+ );
+ }
+
+ .logo {
+ &.logo-alternative-to,
+ &.logo-dev-community,
+ &.logo-hacker-news,
+ &.logo-openalternative,
+ &.logo-privacy-tools,
+ &.logo-reddit,
+ &.logo-sackgeld,
+ &.logo-selfh-st,
+ &.logo-sourceforge,
+ &.logo-umbrel,
+ &.logo-unraid {
+ background-color: rgba(var(--light-primary-text));
+ }
+ }
+ }
+}
diff --git a/libs/ui/src/lib/logo-carousel/logo-carousel.component.stories.ts b/libs/ui/src/lib/logo-carousel/logo-carousel.component.stories.ts
new file mode 100644
index 000000000..a4e88df98
--- /dev/null
+++ b/libs/ui/src/lib/logo-carousel/logo-carousel.component.stories.ts
@@ -0,0 +1,13 @@
+import type { Meta, StoryObj } from '@storybook/angular';
+
+import { GfLogoCarouselComponent } from './logo-carousel.component';
+
+const meta: Meta = {
+ title: 'Logo Carousel',
+ component: GfLogoCarouselComponent
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {};
diff --git a/libs/ui/src/lib/logo-carousel/logo-carousel.component.ts b/libs/ui/src/lib/logo-carousel/logo-carousel.component.ts
new file mode 100644
index 000000000..d7d3fa6af
--- /dev/null
+++ b/libs/ui/src/lib/logo-carousel/logo-carousel.component.ts
@@ -0,0 +1,110 @@
+import { CommonModule } from '@angular/common';
+import { ChangeDetectionStrategy, Component } from '@angular/core';
+
+import { LogoItem } from './interfaces/interfaces';
+
+@Component({
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [CommonModule],
+ selector: 'gf-logo-carousel',
+ styleUrls: ['./logo-carousel.component.scss'],
+ templateUrl: './logo-carousel.component.html'
+})
+export class GfLogoCarouselComponent {
+ public readonly logos: LogoItem[] = [
+ {
+ className: 'logo-alternative-to',
+ isMask: true,
+ name: 'AlternativeTo',
+ title: 'AlternativeTo - Crowdsourced software recommendations',
+ url: 'https://alternativeto.net'
+ },
+ {
+ className: 'logo-awesome',
+ name: 'Awesome Selfhosted',
+ title:
+ 'Awesome-Selfhosted: A list of Free Software network services and web applications which can be hosted on your own servers',
+ url: 'https://github.com/awesome-selfhosted/awesome-selfhosted'
+ },
+ {
+ className: 'logo-dev-community',
+ isMask: true,
+ name: 'DEV Community',
+ title:
+ 'DEV Community - A constructive and inclusive social network for software developers',
+ url: 'https://dev.to'
+ },
+ {
+ className: 'logo-hacker-news',
+ isMask: true,
+ name: 'Hacker News',
+ title: 'Hacker News',
+ url: 'https://news.ycombinator.com'
+ },
+ {
+ className: 'logo-openalternative',
+ isMask: true,
+ name: 'OpenAlternative',
+ title: 'OpenAlternative: Open Source Alternatives to Popular Software',
+ url: 'https://openalternative.co'
+ },
+ {
+ className: 'logo-privacy-tools',
+ isMask: true,
+ name: 'Privacy Tools',
+ title: 'Privacy Tools: Software Alternatives and Encryption',
+ url: 'https://www.privacytools.io'
+ },
+ {
+ className: 'logo-product-hunt',
+ name: 'Product Hunt',
+ title: 'Product Hunt – The best new products in tech.',
+ url: 'https://www.producthunt.com'
+ },
+ {
+ className: 'logo-reddit',
+ isMask: true,
+ name: 'Reddit',
+ title: 'Reddit - Dive into anything',
+ url: 'https://www.reddit.com'
+ },
+ {
+ className: 'logo-sackgeld',
+ isMask: true,
+ name: 'Sackgeld',
+ title: 'Sackgeld.com – Apps für ein höheres Sackgeld',
+ url: 'https://www.sackgeld.com'
+ },
+ {
+ className: 'logo-selfh-st',
+ isMask: true,
+ name: 'selfh.st',
+ title: 'selfh.st — Self-hosted content and software',
+ url: 'https://selfh.st'
+ },
+ {
+ className: 'logo-sourceforge',
+ isMask: true,
+ name: 'SourceForge',
+ title:
+ 'SourceForge: The Complete Open-Source and Business Software Platform',
+ url: 'https://sourceforge.net'
+ },
+ {
+ className: 'logo-umbrel',
+ isMask: true,
+ name: 'Umbrel',
+ title: 'Umbrel — A personal server OS for self-hosting',
+ url: 'https://umbrel.com'
+ },
+ {
+ className: 'logo-unraid',
+ isMask: true,
+ name: 'Unraid',
+ title: 'Unraid | Unleash Your Hardware',
+ url: 'https://unraid.net'
+ }
+ ];
+
+ public readonly logosRepeated = [...this.logos, ...this.logos];
+}