summaryrefslogtreecommitdiff
path: root/src/components/hero/Carousel.svelte
blob: 218813be48f3de6ee35fa631513bd067de6a60e0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
<!--
@component
Component for rendering a carousel of `Hero.svelte` components in a way taht's decoupled from
any particular data model
-->
<script lang="ts" generics="Item">
    import type { Opt } from '@jet/environment/types/optional';
    import type { Artwork, Shelf } from '@jet-app/app-store/api/models';

    import HorizontalShelf from '~/components/jet/shelf/HorizontalShelf.svelte';
    import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
    import { intersectionObserver } from '@amp/web-app-components/src/actions/intersection-observer';
    import mediaQueries from '~/utils/media-queries';
    import { sidebarIsHidden } from '@amp/web-app-components/src/stores/sidebar-hidden';
    import HeroCarouselBackgroundPortal, {
        id as portalId,
    } from '~/components/hero/CarouselBackgroundPortal.svelte';
    import AmbientBackgroundArtwork from '~/components/AmbientBackgroundArtwork.svelte';
    import portal from '~/utils/portal';
    import { carouselMediaStyle } from '~/stores/carousel-media-style';

    interface $$Slots {
        default: {
            /**
             * The `Item` to render as a `Hero` in the carousel
             */
            item: Item;
        };
    }

    /**
     * The shelf being rendered
     *
     * Used to derrive any shelf-specific presentation
     */
    export let shelf: Shelf;

    /**
     * The items to render in the hero carousel
     *
     * This is decoupled from `shelf` to avoid assuming that `shelf.items` is, itself,
     * the set of items that we need to present; some shelves model their items as chilren
     * of the first shelf item.
     */
    export let items: Item[];

    /**
     * Callback that determines the "background artwork" to use behind the
     * active `Hero` for the given `Item`
     */
    export let deriveBackgroundArtworkFromItem: (item: Item) => Opt<Artwork>;

    $: gridRows = shelf.rowsPerColumn ?? undefined;
    $: isXSmallViewport = $mediaQueries === 'xsmall';

    let activeIndex: number | undefined = 0;

    function createIntersectionObserverCallback(index: number) {
        return (isIntersectingViewport: boolean) => {
            if (isIntersectingViewport) {
                // Many different types of `item`s can be rendered in this Carousel, and all those
                // different items have different ways of determining whether or not the background
                // is dark or light, so we are running through all the options here.
                const { style, mediaOverlayStyle, isMediaDark } = items[
                    index
                ] as any;
                const fallbackStyle = 'dark';
                let derivedStyle;

                if (typeof isMediaDark !== 'undefined') {
                    derivedStyle = isMediaDark ? 'dark' : 'light';
                }

                carouselMediaStyle.set(
                    style || mediaOverlayStyle || derivedStyle || fallbackStyle,
                );

                activeIndex = index;
            }
        };
    }
</script>

<HeroCarouselBackgroundPortal />

<ShelfWrapper {shelf} --shelfGridGutterWidth="0">
    <HorizontalShelf
        {gridRows}
        {items}
        --shelfScrollPaddingInline="0"
        --grid-max-content-xsmall={!$sidebarIsHidden
            ? 'calc(100% + 50px)'
            : '100vw'}
        gridType="Spotlight"
        let:item
        let:index
    >
        {#if isXSmallViewport}
            <div
                use:intersectionObserver={{
                    callback: createIntersectionObserverCallback(index),
                    threshold: 0.5,
                }}
            >
                <slot {item} />
            </div>
        {:else}
            <div
                use:intersectionObserver={{
                    callback: createIntersectionObserverCallback(index),
                    threshold: 0,
                }}
            >
                {#if !import.meta.env.SSR}
                    {@const backgroundArtwork =
                        deriveBackgroundArtworkFromItem(item)}

                    {#if backgroundArtwork}
                        <div use:portal={portalId}>
                            <AmbientBackgroundArtwork
                                artwork={backgroundArtwork}
                                active={activeIndex === index}
                            />
                        </div>
                    {/if}
                {/if}

                <slot {item} />
            </div>
        {/if}
    </HorizontalShelf>
</ShelfWrapper>