Zerotask/rust-jungle

View on GitHub
src/components/lesson/lesson.svelte

Summary

Maintainability
Test Coverage
<script lang="ts">
    import { onMount } from 'svelte';
    import { page } from '$app/stores';
    import { goto } from '$app/navigation';
    import Playground from '$components/playground.svelte';
    import FerrisNormal from '$components/ferris/normal.svelte';
    import FerrisHappy from '$components/ferris/happy.svelte';
    import FurtherInformation from '$components/furtherInformation.svelte';
    import InternalLink from '$components/internalLink.svelte';
    import lastLessonStore from '$stores/lastLesson';
    import LessonsStore from '$stores/lessons';
    import { buildTitle } from '$lib/lessons';
    import SocialMediaShare from '$components/lesson/socialMediaShare.svelte';
    export let index = 1;
    export let title: string | null = null;
    export let summary: string | null = null;
    export let tags: string[] | string = [];
    export let previous: string | null = null;
    export let next: string | null = null;
    export let playgroundUrl: string | null = null;
    export let furtherInformationUrls: string | string[] | null = null;

    let isIntroduction = false;
    let isSummary = false;
    let lastStage: number;
    const url: string = encodeURI(`https://${$page.host}${$page.path}`);

    // for compatibility. the lessons endpoint needs a string, not an array.
    if (typeof furtherInformationUrls === 'string') {
        furtherInformationUrls = furtherInformationUrls.split(' ');
    }

    if (!Array.isArray(tags)) {
        tags = tags.split(' ');
    }

    const [_language, _currentPage, currentStage, currentLesson] = $page.path
        .split('/')
        .filter(Boolean);

    // since the 'index' is dropped.
    if (!currentLesson) {
        isIntroduction = true;
    }

    const stage = parseInt(currentStage, 10);
    isSummary = currentLesson === 'summary';

    let lessonsPerStage = [];

    $: fullTitle = buildTitle(stage, index, title);

    let xDown = null;
    let yDown = null;

    onMount(() => {
        // document.addEventListener('touchstart', handleTouchStart, { once: true });
        // document.addEventListener('touchmove', handleTouchMove, { once: true });

        const stageKeys = Object.keys($LessonsStore.stages);
        lastStage = parseInt(stageKeys[stageKeys.length - 1], 10);

        // Automatically extend navigtion destination for introduction and summary
        if (isIntroduction) {
            index = 1;
            title = `Introduction - ${title}`;
            next = `${stage}/${next}`;

            if (stage > 1) {
                previous = `${stage - 1}/summary`;
            }
        } else if (isSummary) {
            title = 'Summary';
            if (stage < lastStage) {
                next = `../${stage + 1}`;
            }
        }

        lessonsPerStage = $LessonsStore.lessons.filter((lesson) => lesson.stage === stage);

        // Remeber the current lesson, to enable a "continue" (testing)
        lastLessonStore.set({
            stage: stage,
            index: index,
            title: title,
            url: $page.path,
            date: new Date()
        });

        // Avoid loosing the focus by the iframe.
        const iframeElement = document.querySelector('iframe');
        if (iframeElement) {
            iframeElement.addEventListener('load', () => setTimeout(() => document.body.focus(), 250));
        }
    });

    function onKeyboardNavigation(event: KeyboardEvent): void {
        document.body.focus();
        const eventKey = event.key;
        let link: string = null;

        if (['Left', 'ArrowLeft', 'Up', 'ArrowUp'].includes(eventKey)) {
            link = previous;
        }
        if (['Right', 'ArrowRight', 'Down', 'ArrowDown'].includes(eventKey)) {
            link = next;
        }
        if (link) {
            goto(link);
        }
    }

    function handleTouchStart(event): void {
        const firstTouch = event.touches[0];
        xDown = firstTouch.clientX;
        yDown = firstTouch.clientY;
    }

    function handleTouchMove(event): void {
        if (!xDown || !yDown) {
            return;
        }

        const xUp = event.touches[0].clientX;
        const yUp = event.touches[0].clientY;
        const xDiff = xDown - xUp;
        const yDiff = yDown - yUp;

        if (Math.abs(xDiff) > Math.abs(yDiff)) {
            // Reset values
            xDown = null;
            yDown = null;
            if (xDiff > 0 && next) {
                goto(next);
            } else if (previous) {
                goto(previous);
            }
        }
    }
</script>

<svelte:head>
    <title>Rust Jungle - {fullTitle}</title>
    <meta property="og:title" content="Rust Jungle - {fullTitle}" />
    <meta name="twitter:title" content="Rust Jungle - {fullTitle}" />
    <meta property="og:url" content={url} />

    {#if summary}
        <meta name="description" content={summary} />
        <meta property="og:description" content={summary} />
        <meta name="twitter:description" content={summary} />
    {/if}

    {#if tags && tags.length > 0}
        <meta name="keywords" content="Rust, Programming, Language, Learning, {tags.join(', ')}" />
    {/if}
</svelte:head>

<svelte:body
    on:keyup|once={onKeyboardNavigation}
    on:touchstart={handleTouchStart}
    on:touchmove={handleTouchMove} />

<div class="pure-g">
    <section class="pure-u-1 pure-u-md-1-2">
        <h1>{fullTitle}</h1>

        <slot />

        {#if tags.length > 0}
            <div id="tags">
                Tags:
                {#each tags as tag}
                    <span><InternalLink href="/en/tags/{tag}">#{tag}</InternalLink></span>
                {/each}
            </div>
        {/if}

        {#if furtherInformationUrls && furtherInformationUrls.length > 0}
            <FurtherInformation links={furtherInformationUrls} />
        {/if}

        <SocialMediaShare title={fullTitle} />
    </section>

    <section class="pure-u-1 pure-u-md-1-2">
        {#if isSummary}
            <FerrisHappy />
        {:else if playgroundUrl}
            <Playground src={playgroundUrl} />
        {:else}
            <FerrisNormal />
        {/if}
    </section>
</div>

<div class="lesson-nav">
    {#if previous}
        <a
            sveltekit:prefetch
            class="previous"
            href={previous}
            rel="prev"
            title="Go to the previous lesson"
            aria-label="Go to the previous lesson">❮ Previous</a
        >
    {:else}
        <span />
    {/if}

    {#if lessonsPerStage.length > 0}
        <div class="stageInformation">
            <p><InternalLink href="/en/stages#{stage}">Stage {stage}</InternalLink></p>
            <p class="stageProgress">{index} / {lessonsPerStage.length}</p>
        </div>
    {/if}

    {#if next}
        <a
            sveltekit:prefetch
            class="next"
            href={next}
            rel="prev"
            title="Go to the next lesson"
            aria-label="Go to the next lesson">Next ❯</a
        >
    {:else}
        <span />
    {/if}
</div>

<style lang="postcss">
    h1 {
        margin-top: 0;
        margin-bottom: 8px;
    }

    .lesson-nav {
        display: flex;
        justify-content: space-between;
        position: fixed;
        bottom: 0;
        width: 100%;
        height: 70px;
        background-color: var(--tertiary-color);
        border-top: 1px solid var(--primary-color);
        margin-left: -20px;
        padding: 10px;
    }

    .lesson-nav a {
        text-decoration: none;
        font-weight: bold;
        font-size: 24px;
        color: var(--text-color);
    }

    .lesson-nav > .next {
        float: right;
        margin-right: 30px;
    }

    .lesson-nav a:hover {
        text-decoration: none;
        color: var(--primary-color);
        font-weight: bold;
    }

    .stageInformation p {
        margin-top: 3px;
        font-weight: bold;
    }

    #tags {
        margin-top: 15px;
    }

    @media screen and (max-width: 840px) {
        section:last-child {
            padding-bottom: 90px;
        }
    }
</style>