npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@etxdev/losys-lib

v1.11.3

Published

This document provides an overview of how to integrate and use the Losys API with Vue 3 components. The components currently use our own CSS framework

Readme

Losys API Integration with Vue 3

This document provides an overview of how to integrate and use the Losys API with Vue 3 components. The components currently use our own CSS framework


Table of Contents

  1. Changelog
  2. Overview
  3. Setup
  4. Components and Data Sources
  5. Event Handlers
  6. Template Integration
  7. Complete Script Code
  8. Complete Template Code

Changelog

1.9.0

  • Enable SSR for the swiper components
  • Fix bug where imageKit url replacement on null object was throwing

1.8.0

  • Fix loading & rendering problems for HighlightComponent and ReferenceDetailComponent
  • Added loader to HighlightComponent

1.7.0

  • Remove unused space in ref detail page
  • Align Image Gallery in ReferenceDetailPage to content instead of window
  • Introduced v-etx-appearance directive to handle will-appear and did-appear to optimize behavior

1.6.12

  • Style attribute in ReferenceDetailPage advantages is now computed, so component updates newly assigned css classes

1.6.11

  • Updated highlight component
  • Updated data-source to trigger loaded$ subject on manually changing the data
  • Introduced setData method to data-source

1.6.10

  • Some fixes

1.6.9

  • Remove href element from teaser card

1.6.8

  • Added attribute style to ReferenceDetail.advantages with values 'bg-secondary' | 'bg-grey-1'
  • HighlightComponent allRefsText optional
  • HighlightComponent always display stripes
  • Pointer cursor on TeaserCards
  • Prevent empty rendered containers in ReferenceDetailComponent
  • Updated examples

1.6.7

  • Added typing for project_categories in Losys Api
  • Updated style for Video Container in ReferenceDetailPage
  • Updated example

1.6.6

  • For FilterComponent added level attribute and css class level-${level} to indicate elements level

1.6.5

  • Updated ReferenceDetailComponent input data. Changed mediaBlock type from Media to EmbedCode. We now expect the videos embed code instead of url. If no mediaBlock is passed, nothing will be rendered.
  • Updated Losys API project video attributes. Added missing attributes
  • Please find all changes in the examples below

1.6.4

  • Implemented HighlightComponent. Added new attribute "id" for HighlightEntry, event handler @reference-clicked and text for the all references link.
  • Notice: on the etx-container for the FilterComponent, please use css class overflow-visible"
  • Introduced loaded$ observable on datasource
  • Added handler for window resize to update and recalculate swiper sizes
  • Added missing div in ReferenceDetailView for the Gallery
  • Please find all listed changes in the examples below

1.6.3

  • Introduced additional information for losys api
  • Added Pro/cons section to reference detail view

1.6.2

  • Introduced new type for attribute project_properties on LosysApiProjectProperty. Pass generic type if wanted.
const projProps = Object.entries(myDataSet[0].project_properties);
const examples = projProps.filter(x => x.value.projectAttributeId === '<<someId>>');
const specificAttributeFound = examples.find(x => (x as LosysApiProjectProperty<boolean >).value === true);

Overview

This integration demonstrates how to use the Losys API to fetch and display project-related data in Vue 3 using reactive data sources and components like HighlightComponent and TeaserListComponent.


Setup

API Service Initialization

The LosysApiService is initialized to handle authentication and fetch data from the Losys API.

const apiService: LosysApiService = new LosysApiService({
  authEndpoint: 'myauth/oauth/token',
  apiHost: 'my-losys-api-host-url',
  authStore: {
    set: (data: LosysApiAuthResult) => {
      localStorage.setItem('losys-auth', JSON.stringify({
        access_token: data.access_token,
        company: undefined
      }));
    },
    get: (): LosysApiAuthResult | undefined => {
      const storedData = localStorage.getItem('losys-auth');
      return storedData ? JSON.parse(storedData) : undefined;
    }
  }
});

apiService.authenticateAndGetBasicInformation().then(() => {
  loadData();
});

Components and Data Sources

Highlight Module Data Source

The highlight module fetches and maps data for the HighlightComponent.

const highlightModuleDataSource: ReactiveAsyncDataSource<HighlightEntry, LosysApiProjectFilter> =
  new ReactiveAsyncDataSource<HighlightEntry, LosysApiProjectFilter>(
    (filter: LosysApiProjectFilter): Promise<HighlightEntry[]> => {
      return new Promise<HighlightEntry[]>((resolve, _reject) => {

        // fetch data from losys api
        apiService.getCustomerProjects({
          ...filter,
          languages: defaultLanguageFilter,
          expand: ['project_images', 'project_categories', 'project_videos', 'project_properties']
        })
          .then(restData => {
            // map result to highlight module entries
            const data: HighlightEntry[] = restData.map(x => {

              // use imagekit urls instead of losys api image urls
              let imageKitUrl = (x.project_images ? x.project_images[0]?.extralarge_image : '')
                .replace('https://api.referenz-verwaltung.ch/static/images/project/', 'https://ik.imagekit.io/equans/losys/');
              imageKitUrl = `${imageKitUrl}?tr=w-3000,ar-3-2,fo-auto`;

              return {
                image: imageKitUrl,
                title: {
                  main: x.title,
                  sub: x.city
                },
                id: 'test'
              };
            });
            resolve(data);
          });

      });
    }
  );

Teaser Module Data Source

The teaser module fetches data for the TeaserListComponent and TeaserSliderComponent.

const teaserModuleDataSource: ReactiveAsyncDataSource<TeaserCard, LosysApiProjectFilter> =
  new ReactiveAsyncDataSource<TeaserCard, LosysApiProjectFilter>(
    (filter: LosysApiProjectFilter): Promise<TeaserCard[]> => {
      return new Promise<TeaserCard[]>((resolve, _reject) => {

        // fetch data from api
        apiService.getCustomerProjects({
          ...filter,
          languages: defaultLanguageFilter,
          expand: [
            'project_images',
            'project_categories',
            'project_videos',
            'project_properties'
          ]
        })
          .then(restData => {
            // map the retrieved data into teaser card data
            const data = restData
              .filter(x => {
                const approvalByMarketingIds = [765, 779, 765, 761, 781];
                const approvalByMarketing = Object.values(x.project_properties ?? {}).find(x => approvalByMarketingIds.includes(x.projectAttributeId));
                return approvalByMarketing?.value === "True";
              })
              .map(x => {

                let imageUrl = (x.project_images && x.project_images?.length > 0 ? x.project_images[0]?.small_image : '')
                  .replace('https://api.referenz-verwaltung.ch/static/images/project/', 'https://ik.imagekit.io/equans/losys/');
                imageUrl = `${imageUrl}?tr=w-1000,ar-3-2,fo-auto`;

                return {
                  properties: {
                    id: `${x.id}`,
                    image: {
                      url: imageUrl
                    },
                    // TODO: map with competences provided through drupal
                    tags: ['Tag 1', 'Tag 2'],
                    title: {
                      small: x.zipcode,
                      main: x.title,
                      sub: x.city
                    }
                  }
                };
              });
            resolve(data);
          });
      });
    }
  );

Event Handlers

Event handlers handle various user interactions.

const handleHighlightAllReferencesClicked = () => {
  alert('should route to all references overview page');
};

const handleHighlightReferenceClicked = (entry: HighlightEntry) => {
  alert(`should route to reference with id: ${entry.id}`);
};

const handleHighlightToggleVariant = (): void => {
  variant.value = variant.value === 'right' ? 'left' : 'right';
};

const handleFiltersChanged = (filters: FilterEntry<(keyof LosysApiProjectFilter)>[]) => {
  let apiFilter: LosysApiProjectFilter = { pageIndex: 0, pageSize: pageSize };
  filters.forEach(x => {
    apiFilter = {
      ...apiFilter,
      [x.id]: x.selected.map(x => x.value)
    };
  });
  teaserModuleDataSource.load(apiFilter, 'overwrite');
};

Template Integration

The template includes HighlightComponent, FilterComponent, and TeaserListComponent.

<template>

  <main>

    <div class="etx-container Container pt-0 pb-25">
      <div class="etx-center Center">
        <h1>Translation Test: {{ $etxtranslate('global.welcome') }}</h1>
        <button @click="$setLanguage('de')">DE</button>
        <button @click="$setLanguage('en')">EN</button>

      </div>
    </div>


    <!-- HIGHLIGHT MODULE -->
    <div class="etx-container Container pt-0 pb-25">
      <div class="etx-center Center">
        <button @click="handleHighlightToggleVariant()">Toggle Variant {{ variant }}</button>

        <HighlightComponent :background="variant" @all-references-clicked="handleHighlightAllReferencesClicked()"
          @reference-clicked="handleHighlightReferenceClicked($event)" :dataSource="highlightModuleDataSource" />
      </div>
    </div>

    <!-- TEASER MODULE AND FILTER -->

    <!-- WARNING: USE style overflow unset in order for filter drawer to work -->
    <div class="etx-container Container pt-0 pb-25 overflow-visible">
      <div class="etx-center Center">
        <FilterComponent :entries="filterEntries" @filters-changed="handleFiltersChanged($event)" />
      </div>
    </div>

    <div class="etx-container Container pt-0 pb-0 overflow-visible">
      <div class="etx-center Center">
        <TeaserListComponent :dataSource="teaserModuleDataSource" @selected="handleTeaserSelected($event)" />
      </div>
    </div>

    <div class="etx-container Container pt-25">
      <div class="etx-center Center">
        <div class="etx-button Button minimal active" @click="handleLoadTeaserListData()">
          <div class="etx-button__text Button__text">
            <span>{{ 'Mehr laden' }}</span>
          </div>
          <div class="etx-button__icon Button__icon-">
            <i class="far fa-long-arrow-down"></i>
          </div>
        </div>
      </div>
    </div>

    <!-- TEASER SWIPER MODULE AND FILTER -->
    <div class="etx-container Container pt-0 pb-0 overflow-visible">
      <div class="etx-center Center">
        <TeaserSliderComponent :dataSource="teaserModuleDataSource" :page-size="pageSize" :id="'references-swiper-1'"
          @selected="handleTeaserSelected($event)" />
      </div>
    </div>

    <div class="etx-spacer"></div>

    <!-- DETAIL REFERENCE VIEW -->
    <ReferenceDetailComponent :data="reactiveTestDetail" @cta-clicked="handleCtaClicked()" />

  </main>
</template>

Complete Script Code

<script setup lang="ts">
import { COMPANIES } from '@/core/constants';
import {
  createReferenceDetail,
  FilterComponent,
  HighlightComponent,
  LosysApiService,
  ReactiveAsyncDataSource,
  ReferenceDetailComponent,
  TeaserListComponent,
  TeaserSliderComponent,
  type FilterEntry,
  type HighlightEntry,
  type HighlightLayoutType,
  type LosysApiAuthResult,
  type LosysApiProjectFilter,
  type ReferenceDetail,
  type TeaserCard
} from '@etx/losys-lib';
import { reactive, ref } from 'vue';

// property definitions

/**
 * init losys api service
 * use etrex.dev proxy for now, which injects client secret into request
 * Setting the authStore param is not needed. Default behavior: token is stored within service instance.
 * If you want to store data elsewhere (as in example) pass a authStore impl.
 */
const apiService: LosysApiService = new LosysApiService({
  authEndpoint: 'https://equans.etrex.live/middleware-losys/proxy.php?endpoint=auth&target=https://api.referenz-verwaltung.ch/oauth/token',
  apiHost: 'https://api.referenz-verwaltung.ch/api',
  authStore: {
    set: (data: LosysApiAuthResult) => {
      localStorage.setItem('losys-auth', JSON.stringify({
        access_token: data.access_token,
        company: undefined! // dont need it
      }));
    },
    get: (): LosysApiAuthResult | undefined => {
      const storedData = localStorage.getItem('losys-auth');
      if (!storedData) return undefined;

      return JSON.parse(storedData) as LosysApiAuthResult;
    }
  }
});
apiService.authenticateAndGetBasicInformation().then(() => {
  loadData();
})

const pageSize: number = 5; // default page size for data sources
let variant = ref<HighlightLayoutType>('left');
let showStripes = ref<boolean>(false);

/**
 * filter entries for filter component
 * Is used to filter data source at a later point
 */
const filterEntries: FilterEntry<keyof LosysApiProjectFilter>[] = [
  {
    id: 'companyIds',
    text: 'Unternehmen',
    items: COMPANIES.map(c => {
      return {
        value: `${c.id}`,
        text: c.text
      }
    }),
    selected: [],
    multiselect: true
  }
];

/**
 * Highlight data source
 * Data source for the highlight component
 *
 */
const highlightModuleDataSource: ReactiveAsyncDataSource<HighlightEntry, LosysApiProjectFilter> =
  new ReactiveAsyncDataSource<HighlightEntry, LosysApiProjectFilter>(
    (filter: LosysApiProjectFilter): Promise<HighlightEntry[]> => {
      return new Promise<HighlightEntry[]>((resolve, _reject) => {

        // fetch data from losys api
        apiService.getCustomerProjects({
          ...filter,
          languages: defaultLanguageFilter,
          expand: ['project_images', 'project_categories', 'project_videos', 'project_properties']
        })
          .then(restData => {
            // map result to highlight module entries
            const data: HighlightEntry[] = restData.map(x => {

              // use imagekit urls instead of losys api image urls
              let imageKitUrl = (x.project_images ? x.project_images[0]?.extralarge_image : '')
                .replace('https://api.referenz-verwaltung.ch/static/images/project/', 'https://ik.imagekit.io/equans/losys/');
              imageKitUrl = `${imageKitUrl}?tr=w-3000,ar-3-2,fo-auto`;

              return {
                image: imageKitUrl,
                title: {
                  main: x.title,
                  sub: x.city
                },
                id: 'test'
              };
            });
            resolve(data);
          });

      });
    }
  );

/**
  * Teaser data source
  * Data source for the teaser list & slider component
  */
const teaserModuleDataSource: ReactiveAsyncDataSource<TeaserCard, LosysApiProjectFilter> =
  new ReactiveAsyncDataSource<TeaserCard, LosysApiProjectFilter>(
    (filter: LosysApiProjectFilter): Promise<TeaserCard[]> => {
      return new Promise<TeaserCard[]>((resolve, _reject) => {

        // fetch data from api
        apiService.getCustomerProjects({
          ...filter,
          expand: [
            'project_images',
            'project_categories'
          ]
        })
          .then(restData => {
            // map the retrieved data into teaser card data
            const data = restData
              .filter(x => {
                const approvalByMarketingIds = [765, 779, 765, 761, 781];
                const approvalByMarketing = Object.values(x.project_properties ?? {}).find(x => approvalByMarketingIds.includes(x.projectAttributeId));
                return approvalByMarketing?.value === "True";
              })
              .map(x => {

                let imageUrl = (x.project_images && x.project_images?.length > 0 ? x.project_images[0]?.small_image : '')
                  .replace('https://api.referenz-verwaltung.ch/static/images/project/', 'https://ik.imagekit.io/equans/losys/');
                imageUrl = `${imageUrl}?tr=w-1000,ar-3-2,fo-auto`;

                return {
                  properties: {
                    id: `${x.id}`,
                    image: {
                      url: imageUrl
                    },
                    // TODO: map with competences provided through drupal
                    tags: ['Tag 1', 'Tag 2'],
                    title: {
                      small: x.zipcode,
                      main: x.title,
                      sub: x.city
                    }
                  }
                };
              });
            resolve(data);
          });
      });
    }
  );

/**
 * Trigged if all references is clicked within highlight module
 */
const handleHighlightAllReferencesClicked = () => {
  alert('should route to all references overview page');
};

/**
 * Toggles the highlight module variant
 * Updates the module to display the block left or right
 */
const handleHighlightToggleVariant = (): void => {
  variant.value = variant.value === 'right' ? 'left' : 'right';
}

/**
 * Toggles wether to display the stripes image container
 * within the highlight module
 */
const handleHighlightToggleStripes = (): void => {
  showStripes.value = !showStripes.value;
}

/**
 * Triggers if teaser card was clicked within highlight module
 * @param card
 */
const handleTeaserSelected = (card: TeaserCard) => {
  alert(`Selected teaser card: ${card.properties.title.main}. Should route to detail page`);
  console.log('should route to entity detail with id:', card.properties.id);
}

/**
 * handle filter component filter changes
 * triggers data source load with new api filter which is composed
 * based on the selected filter items
 * @param filters
 */
const handleFiltersChanged = (filters: FilterEntry<(keyof LosysApiProjectFilter)>[]) => {
  let apiFilter: LosysApiProjectFilter = {
    pageIndex: 0,
    pageSize: pageSize
  };

  // build up api filter
  filters.forEach(x => {
    const filterKey = x.id;
    apiFilter = {
      ...apiFilter,
      [filterKey]: x.selected.map(x => x.value)
    }
  });

  // trigger data source to load data for given filter, overwrite stored data instead of append
  teaserModuleDataSource.load(apiFilter, 'overwrite');
}

/**
 * Triggers load on teaser data source
 * @param filter
 */
const handleLoadTeaserListData = () => {
  // trigger data source to load data for given filter, append result to stored data
  teaserModuleDataSource.load({
    pageSize: pageSize,
    pageIndex: teaserModuleDataSource.activeFilter.pageIndex === 0 ? teaserModuleDataSource.activeFilter.pageIndex : (teaserModuleDataSource.activeFilter.pageIndex * pageSize),
    expand: ['project_images', 'project_categories']
  }, 'append');
}

/**
 * CTA on detail component clicked
 */
const handleCtaClicked = () => {
  alert('clicked cta: do something');
}

/**
 * Define base data for ReferenceDetail component
 */
const reactiveTestDetail: ReferenceDetail = reactive(createReferenceDetail());


const loadData = () => {

  // initial load for highlight module data source
  highlightModuleDataSource.load({
    pageIndex: 0,
    pageSize: 3,
    expand: ['project_images']
  }, 'overwrite');

  // initial load for teaser module data source
  teaserModuleDataSource.load({
    pageIndex: 0,
    pageSize: 5,
    expand: ['project_images']
  }, 'append');

  // initial load for highlight module data source
  highlightModuleDataSource.load({
    pageIndex: 0,
    pageSize: 4,
    expand: ['project_images', 'project_videos']
  }, 'overwrite');

  // initial load for teaser module data source
  teaserModuleDataSource.load({
    pageIndex: 0,
    pageSize: 5,
    expand: ['project_images', 'project_videos']
  }, 'append');

  apiService.getCustomerProjects({
    pageIndex: 0,
    pageSize: 100,
    expand: [
      'project_images',
      'project_videos',
      'project_categories',
      'project_address_contact_persons',
      'project_participating_companies',
      'project_properties',
      'project_type_of_buildings',
      'project_type_of_constructions',
      'project_type_of_works'
    ],
    languages: ['de']
  }).then(data => {

    const x = data[0];

    const props = Object.values(x.project_properties);
    const approvalByMarketingIds = [765, 779, 765, 761, 781];
    const approvalByMarketing = props.find(x => approvalByMarketingIds.includes(x.projectAttributeId));
    const titleSection1 = props.find(x => x.projectAttributeId === 784);
    const leadText = props.find(x => x.projectAttributeId === 785);
    const description1 = props.find(x => x.projectAttributeId === 786);
    const titleSection2 = props.find(x => x.projectAttributeId === 787);
    const description2 = props.find(x => x.projectAttributeId === 788);
    const quote = props.find(x => x.projectAttributeId === 789);
    const quoteLegend = props.find(x => x.projectAttributeId === 790);

    const advantageIds = [791, 792, 800, 814, 822, 830, 838, 846, 854, 862, 870, 878, 886];
    const advantage: string = props.find(x => advantageIds.includes(x.projectAttributeId))?.value;
    const advantages = advantage.split('\r\n');

    if (approvalByMarketing?.value === "False") {
      alert('This is not yet approved by marketing');
    }

    // Try to find Main Image otherwise use first image
    const images = (x.project_images ?? []).length > 0 ? x.project_images ?? [] : [];

    // use imagekit urls instead of losys api image urls
    let imageKitUrls = images.map(img => {
      let imageKitUrl = img.large_image.replace('https://api.referenz-verwaltung.ch/static/images/project/', 'https://ik.imagekit.io/equans/losys/');
      imageKitUrl = `${imageKitUrl}?tr=w-3000,ar-3-2,fo-auto`;
      return imageKitUrl;
    });

    const mainImage = images.find(x => x.isMainImage)?.large_image ??
      (images.length > 0 ? images[0].large_image : undefined);


    /**
     * This should map to the top level competence of each iterated competence
     * We want to display a tag for each top level competence of the reference.
     * Apply the custom cms - losys mapping
     */
    const topLevelTags = x.project_categories?.map(x => {
      const tag: Tag = {
        text: x.category?.titles?.displayLocale
      };
      return tag;
    }) ?? [];

    reactiveTestDetail.hero = {
      title: {
        main: x.title,
        sub: x.city
      },
      image: {
        url: mainImage!
      },
      // split the page navigation into crumbs
      breadcrumbs: [
        {
          text: 'Parent Page',
          url: 'https://parentpage....'
        },
        {
          text: 'Current (Active) Page',
          url: 'https://parentpage/currentpage....'
        }
      ],
      // top level competences tags corresponding to the reference
      tags: topLevelTags
    };

    reactiveTestDetail.intro = {
      title: {
        main: titleSection1?.value,
        sub: leadText?.value
      },
      text: description1?.value,
      wanted: {
        date: {
          text: x.yearOfCompletion.toString()
        },
        location: {
          text: `${x.city} - ${x.address}`,
        },
        title: {
          // static text
          main: 'Zahlen & Fakten'
        }
      }
    };

    reactiveTestDetail.images = imageKitUrls.reverse().map(x => {
      return {
        url: x
      }
    });

    reactiveTestDetail.textBlock = {
      title: {
        main: titleSection2?.value
      },
      content: {
        text: description2?.value
      }
    };

    // would map video from losys but no example available RN

    if (x.project_videos && x.project_videos.length > 0) {
      reactiveTestDetail.mediaBlock = {
        code: x.project_videos[0].embbedIframe
      };
    }


    // map quotes coming from drupal?
    if (quote && quoteLegend) {
      reactiveTestDetail.quotes = [
        {
          person: quoteLegend?.value,
          quote: quote?.value
        }
      ];
    }

    /**
     * For the sake of having an example: Harcoded 
     * Foreach topLevelTag in topLevelTags list the second level competences in tiles
     */
    const subSections: TileTeaserSubSection[] = [
      {
        text: 'Gebäudetechnik',
        url: 'https://www.google.com',
        tiles: [
          {
            text: 'Heizung & Kühlung',
            url: 'https://www.google.com'
          },
          {
            text: 'Lüftung & Klima',
            url: 'https://www.google.com'
          },
          {
            text: 'Sanitär',
            url: 'https://www.google.com'
          },
          {
            text: 'Reinraum',
            url: 'https://www.google.com'
          }
        ]
      }
    ];

    /**
     * Create SubSections for every top level competence mapped to the reference
     * Each SubSction url should route to the reference overview page with active filter of the top level competence
     * Each Tile url within a subscription should route the the references overview page with active filter of selected competence
     */
    reactiveTestDetail.tileTeaser = {
      title: {
        // static text
        main: 'Unsere Leistungen'
      },
      // static text
      text: 'Als Komplettanbieterin für Gebäude, Infrastruktur und Energie gehen wir mit massgeschneiderten Lösungen auf Sie ein.', // static text
      sections: [
        {
          title: {
            main: 'Kompetenzen'
          },
          subSections: subSections
        }
      ]
    };

    reactiveTestDetail.advantages = {
      title: {
        // static text
        main: 'Ihre Vorteile'
      },
      // static text
      text: 'Unsere Kund:innen haben die verschiedenste Bedürfnisse. Unser Anspruch ist es, für jedes Bedürfnis den besten Service zu bieten – passgenau, effizient und zuver­lässig',
      advantages: advantages,
      style: 'bg-secondary'
    };
  });
}

</script>

Complete Template Code

<template>

  <main>

    <!-- HIGHLIGHT MODULE -->
    <div class="etx-container Container pt-0 pb-100">
      <div class="etx-center Center">
        <button @click="handleHighlightToggleVariant()">Toggle Variant {{ variant }}</button>

        <HighlightComponent :background="variant" @all-references-clicked="handleHighlightAllReferencesClicked()"
          @reference-clicked="handleHighlightReferenceClicked($event)" :dataSource="highlightModuleDataSource" />
      </div>
    </div>

    <!-- TEASER MODULE AND FILTER -->

    <!-- WARNING: USE style overflow unset in order for filter drawer to work -->
    <div class="etx-container Container pt-0 pb-25 overflow-visible">
      <div class="etx-center Center">
        <FilterComponent :entries="filterEntries" @filters-changed="handleFiltersChanged($event)" />
      </div>
    </div>

    <div class="etx-container Container pt-0 pb-0 overflow-visible">
      <div class="etx-center Center">
        <TeaserListComponent :dataSource="teaserModuleDataSource" @selected="handleTeaserSelected($event)" />
      </div>
    </div>

    <div class="etx-container Container pt-25">
      <div class="etx-center Center">
        <div class="etx-button Button minimal active" @click="handleLoadTeaserListData()">
          <div class="etx-button__text Button__text">
            <span>{{ 'Mehr laden' }}</span>
          </div>
          <div class="etx-button__icon Button__icon-">
            <i class="far fa-long-arrow-down"></i>
          </div>
        </div>
      </div>
    </div>

    <!-- TEASER SWIPER MODULE AND FILTER -->
    <div class="etx-container Container pt-0 pb-0 overflow-visible">
      <div class="etx-center Center">
        <TeaserSliderComponent :dataSource="teaserModuleDataSource" :page-size="pageSize" :id="'references-swiper-1'"
          @selected="handleTeaserSelected($event)" />
      </div>
    </div>

    <div class="etx-spacer"></div>

    <!-- DETAIL REFERENCE VIEW -->
    <ReferenceDetailComponent :data="reactiveTestDetail" @cta-clicked="handleCtaClicked()" />

  </main>
</template>