mirror of https://github.com/ghostfolio/ghostfolio
				
				
			
			
			
				Browse Source
			
			
			
			
				
		- Add GfLogoCarouselComponent with pure CSS infinite scroll animation - Replace static logo grid with dynamic carousel in landing page - Implement responsive design with mobile/tablet/desktop support - Add accessibility features (hover pause, ARIA labels) - Include dark theme support and webkit prefixes for compatibility - Maintain all existing logo URLs and titlespull/5671/head
				 9 changed files with 4902 additions and 5024 deletions
			
			
		@ -0,0 +1,98 @@ | 
				
			|||||
 | 
					# Logo Carousel Component Implementation | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					## Summary | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					Successfully implemented the infinite logo carousel component as requested in issue #5660. | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					## Files Created | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					### 1. Component Files | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					- `libs/ui/src/lib/logo-carousel/logo-carousel.component.ts` - Main component logic | 
				
			||||
 | 
					- `libs/ui/src/lib/logo-carousel/logo-carousel.component.html` - Template | 
				
			||||
 | 
					- `libs/ui/src/lib/logo-carousel/logo-carousel.component.scss` - Styles with CSS animation | 
				
			||||
 | 
					- `libs/ui/src/lib/logo-carousel/index.ts` - Export file | 
				
			||||
 | 
					- `libs/ui/src/lib/logo-carousel/logo-carousel.component.stories.ts` - Storybook story | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					### 2. Modified Files | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					- `apps/client/src/app/pages/landing/landing-page.component.ts` - Added import and component usage | 
				
			||||
 | 
					- `apps/client/src/app/pages/landing/landing-page.html` - Replaced static logo grid with carousel | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					## Implementation Details | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					### Features Implemented | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					✅ **Pure CSS Animation**: Uses `@keyframes` for infinite horizontal scrolling | 
				
			||||
 | 
					✅ **Responsive Design**: Adapts to different screen sizes (mobile, tablet, desktop) | 
				
			||||
 | 
					✅ **Accessibility**: | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					- Proper ARIA labels | 
				
			||||
 | 
					- Pause animation on hover | 
				
			||||
 | 
					- Screen reader friendly | 
				
			||||
 | 
					  ✅ **Dark Theme Support**: Automatic theme switching | 
				
			||||
 | 
					  ✅ **Performance**: | 
				
			||||
 | 
					- CSS-only animation (no JavaScript) | 
				
			||||
 | 
					- Uses `transform: translateX()` for GPU acceleration | 
				
			||||
 | 
					  ✅ **Seamless Loop**: Duplicates logos for continuous scrolling | 
				
			||||
 | 
					  ✅ **Interactive**: Hover effects with scaling and opacity changes | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					### Technical Approach | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					- **Animation Technique**: Duplicates the logo set and uses CSS `translateX(-50%)` to create seamless infinite scroll | 
				
			||||
 | 
					- **Browser Compatibility**: Added `-webkit-` prefixes for mask properties | 
				
			||||
 | 
					- **Styling**: Copied and adapted existing logo styles from landing page | 
				
			||||
 | 
					- **Theming**: Uses CSS custom properties for theme-aware gradients | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					### Animation Details | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					- **Duration**: 40 seconds for full cycle | 
				
			||||
 | 
					- **Direction**: Left to right horizontal scroll | 
				
			||||
 | 
					- **Easing**: Linear for consistent speed | 
				
			||||
 | 
					- **Interaction**: Pauses on hover for accessibility | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					### CSS Features | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					- Gradient fade masks on left/right edges for smooth visual effect | 
				
			||||
 | 
					- Responsive logo sizing (3rem height on desktop, scales down on mobile) | 
				
			||||
 | 
					- Hover effects with 1.05x scale and opacity fade | 
				
			||||
 | 
					- Proper mask image handling for SVG logos | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					## Integration | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					The component is integrated into the landing page replacing the previous static 4-column logo grid. The "As seen in" text remains above the carousel. | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					## Browser Support | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					- Modern browsers with CSS mask support | 
				
			||||
 | 
					- Fallback styles for older browsers | 
				
			||||
 | 
					- Responsive design for all device sizes | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					## Usage | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					```html | 
				
			||||
 | 
					<gf-logo-carousel></gf-logo-carousel> | 
				
			||||
 | 
					``` | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					The component is self-contained and requires no additional configuration. | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					## Testing Recommendations | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					1. Test on different screen sizes | 
				
			||||
 | 
					2. Verify smooth animation performance | 
				
			||||
 | 
					3. Check accessibility with screen readers | 
				
			||||
 | 
					4. Test dark/light theme switching | 
				
			||||
 | 
					5. Verify all logo links work correctly | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					## Next Steps | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					1. Install dependencies: `npm install` | 
				
			||||
 | 
					2. Build application: `npm run build:production` | 
				
			||||
 | 
					3. Test in browser: `npm run start:client` | 
				
			||||
 | 
					4. View in Storybook: `npm run start:storybook` | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					The implementation follows the exact requirements from issue #5660: | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					- ✅ Standalone Angular component at `@ghostfolio/ui/logo-carousel` | 
				
			||||
 | 
					- ✅ Pure CSS infinite scrolling animation | 
				
			||||
 | 
					- ✅ Integrated into landing page replacing static logos | 
				
			||||
@ -0,0 +1 @@ | 
				
			|||||
 | 
					export * from './logo-carousel.component'; | 
				
			||||
@ -0,0 +1,16 @@ | 
				
			|||||
 | 
					<div class="logo-carousel-container"> | 
				
			||||
 | 
					  <div class="logo-carousel-track"> | 
				
			||||
 | 
					    @for (logo of duplicatedLogos; track $index) { | 
				
			||||
 | 
					      <div class="logo-carousel-item"> | 
				
			||||
 | 
					        <a | 
				
			||||
 | 
					          class="d-block logo" | 
				
			||||
 | 
					          target="_blank" | 
				
			||||
 | 
					          [attr.aria-label]="logo.name" | 
				
			||||
 | 
					          [class]="logo.className + (logo.isMask ? ' mask' : '')" | 
				
			||||
 | 
					          [href]="logo.url" | 
				
			||||
 | 
					          [title]="logo.title" | 
				
			||||
 | 
					        ></a> | 
				
			||||
 | 
					      </div> | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					  </div> | 
				
			||||
 | 
					</div> | 
				
			||||
@ -0,0 +1,248 @@ | 
				
			|||||
 | 
					:host { | 
				
			||||
 | 
					  display: block; | 
				
			||||
 | 
					  width: 100%; | 
				
			||||
 | 
					  overflow: hidden; | 
				
			||||
 | 
					  position: relative; | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					.logo-carousel-container { | 
				
			||||
 | 
					  width: 100%; | 
				
			||||
 | 
					  overflow: hidden; | 
				
			||||
 | 
					  position: relative; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					  // Fade gradient mask for smooth edges | 
				
			||||
 | 
					  &::before, | 
				
			||||
 | 
					  &::after { | 
				
			||||
 | 
					    content: ''; | 
				
			||||
 | 
					    position: absolute; | 
				
			||||
 | 
					    top: 0; | 
				
			||||
 | 
					    width: 100px; | 
				
			||||
 | 
					    height: 100%; | 
				
			||||
 | 
					    z-index: 2; | 
				
			||||
 | 
					    pointer-events: none; | 
				
			||||
 | 
					  } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					  &::before { | 
				
			||||
 | 
					    left: 0; | 
				
			||||
 | 
					    background: linear-gradient( | 
				
			||||
 | 
					      to right, | 
				
			||||
 | 
					      rgba(var(--palette-background-background), 1) 0%, | 
				
			||||
 | 
					      rgba(var(--palette-background-background), 0) 100% | 
				
			||||
 | 
					    ); | 
				
			||||
 | 
					  } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					  &::after { | 
				
			||||
 | 
					    right: 0; | 
				
			||||
 | 
					    background: linear-gradient( | 
				
			||||
 | 
					      to left, | 
				
			||||
 | 
					      rgba(var(--palette-background-background), 1) 0%, | 
				
			||||
 | 
					      rgba(var(--palette-background-background), 0) 100% | 
				
			||||
 | 
					    ); | 
				
			||||
 | 
					  } | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					.logo-carousel-track { | 
				
			||||
 | 
					  display: flex; | 
				
			||||
 | 
					  align-items: center; | 
				
			||||
 | 
					  animation: scroll 40s linear infinite; | 
				
			||||
 | 
					  width: fit-content; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					  // Pause animation on hover for accessibility | 
				
			||||
 | 
					  &:hover { | 
				
			||||
 | 
					    animation-play-state: paused; | 
				
			||||
 | 
					  } | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					.logo-carousel-item { | 
				
			||||
 | 
					  flex-shrink: 0; | 
				
			||||
 | 
					  padding: 1rem 2rem; | 
				
			||||
 | 
					  display: flex; | 
				
			||||
 | 
					  align-items: center; | 
				
			||||
 | 
					  justify-content: center; | 
				
			||||
 | 
					  min-width: 200px; | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					.logo { | 
				
			||||
 | 
					  height: 3rem; | 
				
			||||
 | 
					  width: 7.5rem; | 
				
			||||
 | 
					  transition: | 
				
			||||
 | 
					    opacity 0.3s ease, | 
				
			||||
 | 
					    transform 0.3s ease; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					  &:hover { | 
				
			||||
 | 
					    opacity: 0.8; | 
				
			||||
 | 
					    transform: scale(1.05); | 
				
			||||
 | 
					  } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					  &.mask { | 
				
			||||
 | 
					    background-color: rgba(var(--dark-secondary-text)); | 
				
			||||
 | 
					    -webkit-mask-position: center; | 
				
			||||
 | 
					    mask-position: center; | 
				
			||||
 | 
					    -webkit-mask-repeat: no-repeat; | 
				
			||||
 | 
					    mask-repeat: no-repeat; | 
				
			||||
 | 
					    -webkit-mask-size: contain; | 
				
			||||
 | 
					    mask-size: contain; | 
				
			||||
 | 
					  } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					  // Logo-specific styles (copied from landing-page.scss) | 
				
			||||
 | 
					  &.logo-alternative-to { | 
				
			||||
 | 
					    -webkit-mask-image: url('/assets/images/logo-alternative-to.svg'); | 
				
			||||
 | 
					    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 { | 
				
			||||
 | 
					    -webkit-mask-image: url('/assets/images/logo-dev-community.svg'); | 
				
			||||
 | 
					    mask-image: url('/assets/images/logo-dev-community.svg'); | 
				
			||||
 | 
					  } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					  &.logo-hacker-news { | 
				
			||||
 | 
					    -webkit-mask-image: url('/assets/images/logo-hacker-news.svg'); | 
				
			||||
 | 
					    mask-image: url('/assets/images/logo-hacker-news.svg'); | 
				
			||||
 | 
					  } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					  &.logo-openalternative { | 
				
			||||
 | 
					    -webkit-mask-image: url('/assets/images/logo-openalternative.svg'); | 
				
			||||
 | 
					    mask-image: url('/assets/images/logo-openalternative.svg'); | 
				
			||||
 | 
					  } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					  &.logo-privacy-tools { | 
				
			||||
 | 
					    -webkit-mask-image: url('/assets/images/logo-privacy-tools.svg'); | 
				
			||||
 | 
					    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 { | 
				
			||||
 | 
					    -webkit-mask-image: url('/assets/images/logo-reddit.svg'); | 
				
			||||
 | 
					    mask-image: url('/assets/images/logo-reddit.svg'); | 
				
			||||
 | 
					    max-height: 1rem; | 
				
			||||
 | 
					  } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					  &.logo-sackgeld { | 
				
			||||
 | 
					    -webkit-mask-image: url('/assets/images/logo-sackgeld.png'); | 
				
			||||
 | 
					    mask-image: url('/assets/images/logo-sackgeld.png'); | 
				
			||||
 | 
					  } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					  &.logo-selfh-st { | 
				
			||||
 | 
					    -webkit-mask-image: url('/assets/images/logo-selfh-st.svg'); | 
				
			||||
 | 
					    mask-image: url('/assets/images/logo-selfh-st.svg'); | 
				
			||||
 | 
					    max-height: 1.25rem; | 
				
			||||
 | 
					  } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					  &.logo-sourceforge { | 
				
			||||
 | 
					    -webkit-mask-image: url('/assets/images/logo-sourceforge.svg'); | 
				
			||||
 | 
					    mask-image: url('/assets/images/logo-sourceforge.svg'); | 
				
			||||
 | 
					  } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					  &.logo-umbrel { | 
				
			||||
 | 
					    -webkit-mask-image: url('/assets/images/logo-umbrel.svg'); | 
				
			||||
 | 
					    mask-image: url('/assets/images/logo-umbrel.svg'); | 
				
			||||
 | 
					    max-height: 1.5rem; | 
				
			||||
 | 
					  } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					  &.logo-unraid { | 
				
			||||
 | 
					    -webkit-mask-image: url('/assets/images/logo-unraid.svg'); | 
				
			||||
 | 
					    mask-image: url('/assets/images/logo-unraid.svg'); | 
				
			||||
 | 
					  } | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					// Animation keyframes for infinite horizontal scroll | 
				
			||||
 | 
					@keyframes scroll { | 
				
			||||
 | 
					  0% { | 
				
			||||
 | 
					    transform: translateX(0); | 
				
			||||
 | 
					  } | 
				
			||||
 | 
					  100% { | 
				
			||||
 | 
					    // Move by exactly half the width (since we duplicated the logos) | 
				
			||||
 | 
					    transform: translateX(-50%); | 
				
			||||
 | 
					  } | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					// Dark theme support | 
				
			||||
 | 
					:host-context(.theme-dark) { | 
				
			||||
 | 
					  .logo-carousel-container { | 
				
			||||
 | 
					    &::before { | 
				
			||||
 | 
					      background: linear-gradient( | 
				
			||||
 | 
					        to right, | 
				
			||||
 | 
					        rgba(var(--palette-background-background-dark), 1) 0%, | 
				
			||||
 | 
					        rgba(var(--palette-background-background-dark), 0) 100% | 
				
			||||
 | 
					      ); | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    &::after { | 
				
			||||
 | 
					      background: linear-gradient( | 
				
			||||
 | 
					        to left, | 
				
			||||
 | 
					        rgba(var(--palette-background-background-dark), 1) 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)); | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					  } | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					// Responsive adjustments | 
				
			||||
 | 
					@media (max-width: 768px) { | 
				
			||||
 | 
					  .logo-carousel-item { | 
				
			||||
 | 
					    min-width: 150px; | 
				
			||||
 | 
					    padding: 0.75rem 1.5rem; | 
				
			||||
 | 
					  } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					  .logo { | 
				
			||||
 | 
					    height: 2.5rem; | 
				
			||||
 | 
					    width: 6rem; | 
				
			||||
 | 
					  } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					  .logo-carousel-container { | 
				
			||||
 | 
					    &::before, | 
				
			||||
 | 
					    &::after { | 
				
			||||
 | 
					      width: 50px; | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					  } | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					@media (max-width: 576px) { | 
				
			||||
 | 
					  .logo-carousel-item { | 
				
			||||
 | 
					    min-width: 120px; | 
				
			||||
 | 
					    padding: 0.5rem 1rem; | 
				
			||||
 | 
					  } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					  .logo { | 
				
			||||
 | 
					    height: 2rem; | 
				
			||||
 | 
					    width: 5rem; | 
				
			||||
 | 
					  } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					  .logo-carousel-container { | 
				
			||||
 | 
					    &::before, | 
				
			||||
 | 
					    &::after { | 
				
			||||
 | 
					      width: 30px; | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					  } | 
				
			||||
 | 
					} | 
				
			||||
@ -0,0 +1,13 @@ | 
				
			|||||
 | 
					import type { Meta, StoryObj } from '@storybook/angular'; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					import { GfLogoCarouselComponent } from './logo-carousel.component'; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					const meta: Meta<GfLogoCarouselComponent> = { | 
				
			||||
 | 
					  title: 'Components/Logo Carousel', | 
				
			||||
 | 
					  component: GfLogoCarouselComponent | 
				
			||||
 | 
					}; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					export default meta; | 
				
			||||
 | 
					type Story = StoryObj<GfLogoCarouselComponent>; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					export const Default: Story = {}; | 
				
			||||
@ -0,0 +1,117 @@ | 
				
			|||||
 | 
					import { CommonModule } from '@angular/common'; | 
				
			||||
 | 
					import { ChangeDetectionStrategy, Component } from '@angular/core'; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					export interface LogoItem { | 
				
			||||
 | 
					  name: string; | 
				
			||||
 | 
					  url: string; | 
				
			||||
 | 
					  title: string; | 
				
			||||
 | 
					  className: string; | 
				
			||||
 | 
					  isMask?: boolean; | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					@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[] = [ | 
				
			||||
 | 
					    { | 
				
			||||
 | 
					      name: 'AlternativeTo', | 
				
			||||
 | 
					      url: 'https://alternativeto.net', | 
				
			||||
 | 
					      title: 'AlternativeTo - Crowdsourced software recommendations', | 
				
			||||
 | 
					      className: 'logo-alternative-to', | 
				
			||||
 | 
					      isMask: true | 
				
			||||
 | 
					    }, | 
				
			||||
 | 
					    { | 
				
			||||
 | 
					      name: 'Awesome Selfhosted', | 
				
			||||
 | 
					      url: 'https://github.com/awesome-selfhosted/awesome-selfhosted', | 
				
			||||
 | 
					      title: | 
				
			||||
 | 
					        'Awesome-Selfhosted: A list of Free Software network services and web applications which can be hosted on your own servers', | 
				
			||||
 | 
					      className: 'logo-awesome' | 
				
			||||
 | 
					    }, | 
				
			||||
 | 
					    { | 
				
			||||
 | 
					      name: 'DEV Community', | 
				
			||||
 | 
					      url: 'https://dev.to', | 
				
			||||
 | 
					      title: | 
				
			||||
 | 
					        'DEV Community - A constructive and inclusive social network for software developers', | 
				
			||||
 | 
					      className: 'logo-dev-community', | 
				
			||||
 | 
					      isMask: true | 
				
			||||
 | 
					    }, | 
				
			||||
 | 
					    { | 
				
			||||
 | 
					      name: 'Hacker News', | 
				
			||||
 | 
					      url: 'https://news.ycombinator.com', | 
				
			||||
 | 
					      title: 'Hacker News', | 
				
			||||
 | 
					      className: 'logo-hacker-news', | 
				
			||||
 | 
					      isMask: true | 
				
			||||
 | 
					    }, | 
				
			||||
 | 
					    { | 
				
			||||
 | 
					      name: 'OpenAlternative', | 
				
			||||
 | 
					      url: 'https://openalternative.co', | 
				
			||||
 | 
					      title: 'OpenAlternative: Open Source Alternatives to Popular Software', | 
				
			||||
 | 
					      className: 'logo-openalternative', | 
				
			||||
 | 
					      isMask: true | 
				
			||||
 | 
					    }, | 
				
			||||
 | 
					    { | 
				
			||||
 | 
					      name: 'Privacy Tools', | 
				
			||||
 | 
					      url: 'https://www.privacytools.io', | 
				
			||||
 | 
					      title: 'Privacy Tools: Software Alternatives and Encryption', | 
				
			||||
 | 
					      className: 'logo-privacy-tools', | 
				
			||||
 | 
					      isMask: true | 
				
			||||
 | 
					    }, | 
				
			||||
 | 
					    { | 
				
			||||
 | 
					      name: 'Product Hunt', | 
				
			||||
 | 
					      url: 'https://www.producthunt.com', | 
				
			||||
 | 
					      title: 'Product Hunt – The best new products in tech.', | 
				
			||||
 | 
					      className: 'logo-product-hunt' | 
				
			||||
 | 
					    }, | 
				
			||||
 | 
					    { | 
				
			||||
 | 
					      name: 'Reddit', | 
				
			||||
 | 
					      url: 'https://www.reddit.com', | 
				
			||||
 | 
					      title: 'Reddit - Dive into anything', | 
				
			||||
 | 
					      className: 'logo-reddit', | 
				
			||||
 | 
					      isMask: true | 
				
			||||
 | 
					    }, | 
				
			||||
 | 
					    { | 
				
			||||
 | 
					      name: 'Sackgeld', | 
				
			||||
 | 
					      url: 'https://www.sackgeld.com', | 
				
			||||
 | 
					      title: 'Sackgeld.com – Apps für ein höheres Sackgeld', | 
				
			||||
 | 
					      className: 'logo-sackgeld', | 
				
			||||
 | 
					      isMask: true | 
				
			||||
 | 
					    }, | 
				
			||||
 | 
					    { | 
				
			||||
 | 
					      name: 'selfh.st', | 
				
			||||
 | 
					      url: 'https://selfh.st', | 
				
			||||
 | 
					      title: 'selfh.st — Self-hosted content and software', | 
				
			||||
 | 
					      className: 'logo-selfh-st', | 
				
			||||
 | 
					      isMask: true | 
				
			||||
 | 
					    }, | 
				
			||||
 | 
					    { | 
				
			||||
 | 
					      name: 'SourceForge', | 
				
			||||
 | 
					      url: 'https://sourceforge.net', | 
				
			||||
 | 
					      title: | 
				
			||||
 | 
					        'SourceForge: The Complete Open-Source and Business Software Platform', | 
				
			||||
 | 
					      className: 'logo-sourceforge', | 
				
			||||
 | 
					      isMask: true | 
				
			||||
 | 
					    }, | 
				
			||||
 | 
					    { | 
				
			||||
 | 
					      name: 'Umbrel', | 
				
			||||
 | 
					      url: 'https://umbrel.com', | 
				
			||||
 | 
					      title: 'Umbrel — A personal server OS for self-hosting', | 
				
			||||
 | 
					      className: 'logo-umbrel', | 
				
			||||
 | 
					      isMask: true | 
				
			||||
 | 
					    }, | 
				
			||||
 | 
					    { | 
				
			||||
 | 
					      name: 'Unraid', | 
				
			||||
 | 
					      url: 'https://unraid.net', | 
				
			||||
 | 
					      title: 'Unraid | Unleash Your Hardware', | 
				
			||||
 | 
					      className: 'logo-unraid', | 
				
			||||
 | 
					      isMask: true | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					  ]; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					  // Duplicate logos for seamless infinite scroll
 | 
				
			||||
 | 
					  public readonly duplicatedLogos = [...this.logos, ...this.logos]; | 
				
			||||
 | 
					} | 
				
			||||
								
									
										File diff suppressed because it is too large
									
								
							
						
					
					Loading…
					
					
				
		Reference in new issue