forked from MapComplete/MapComplete
		
	Merge branch 'develop' into feature/eslint
This commit is contained in:
		
						commit
						ce897d28df
					
				
					 1450 changed files with 20081 additions and 16531 deletions
				
			
		| 
						 | 
				
			
			@ -35,7 +35,7 @@
 | 
			
		|||
        src={`https://raw.githubusercontent.com/pietervdvn/MapComplete-data/main/community_index/${resource.type}.svg`}
 | 
			
		||||
      />
 | 
			
		||||
      <div class="flex flex-col">
 | 
			
		||||
        <a href={resource.resolved.url} target="_blank" rel="noreferrer nofollow" class="font-bold">
 | 
			
		||||
        <a href={resource.resolved.url} target="_blank" rel="noreferrer nofollow noopener" class="font-bold">
 | 
			
		||||
          {resource.resolved.name ?? resource.resolved.url}
 | 
			
		||||
        </a>
 | 
			
		||||
        {resource.resolved?.description}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -102,7 +102,7 @@ export default class CopyrightPanel extends Combine {
 | 
			
		|||
                        let bgAttr: BaseUIElement | string = undefined
 | 
			
		||||
                        if (attrText && attrUrl) {
 | 
			
		||||
                            bgAttr =
 | 
			
		||||
                                "<a href='" + attrUrl + "' target='_blank'>" + attrText + "</a>"
 | 
			
		||||
                                "<a href='" + attrUrl + "' target='_blank' rel='noopener'>" + attrText + "</a>"
 | 
			
		||||
                        } else if (attrUrl) {
 | 
			
		||||
                            bgAttr = attrUrl
 | 
			
		||||
                        } else {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										121
									
								
								src/UI/BigComponents/ShareScreen.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								src/UI/BigComponents/ShareScreen.svelte
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,121 @@
 | 
			
		|||
<script lang="ts">/**
 | 
			
		||||
 * A screen showing:
 | 
			
		||||
 * - A link to share the current view
 | 
			
		||||
 * - Some query parameters that can be enabled/disabled
 | 
			
		||||
 * - The code to embed MC as IFrame
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import ThemeViewState from "../../Models/ThemeViewState";
 | 
			
		||||
import { QueryParameters } from "../../Logic/Web/QueryParameters";
 | 
			
		||||
import Tr from "../Base/Tr.svelte";
 | 
			
		||||
import Translations from "../i18n/Translations";
 | 
			
		||||
import { Utils } from "../../Utils";
 | 
			
		||||
import Svg from "../../Svg";
 | 
			
		||||
import ToSvelte from "../Base/ToSvelte.svelte";
 | 
			
		||||
import { DocumentDuplicateIcon } from "@rgossiaux/svelte-heroicons/outline";
 | 
			
		||||
 | 
			
		||||
export let state: ThemeViewState;
 | 
			
		||||
const tr = Translations.t.general.sharescreen;
 | 
			
		||||
 | 
			
		||||
let url = window.location;
 | 
			
		||||
let linkToShare: string = undefined;
 | 
			
		||||
/**
 | 
			
		||||
 * In some cases (local deploys, custom themes), we need to set the URL to `/theme.html?layout=xyz` instead of `/xyz?...`
 | 
			
		||||
 */
 | 
			
		||||
let needsThemeRedirect = url.port !== "" || url.hostname.match(/^[0-9]/) || !state.layout.official;
 | 
			
		||||
let layoutId = state.layout.id;
 | 
			
		||||
let baseLink = url.protocol + "//" + url.host + "/" + (needsThemeRedirect ? "theme.html?layout=" + layoutId + "&" : layoutId + "?");
 | 
			
		||||
 | 
			
		||||
let showWelcomeMessage = true;
 | 
			
		||||
let enableLogin = true;
 | 
			
		||||
$: {
 | 
			
		||||
  const layout = state.layout;
 | 
			
		||||
  let excluded = Utils.NoNull([
 | 
			
		||||
    showWelcomeMessage ? undefined : "fs-welcome-message",
 | 
			
		||||
    enableLogin ? undefined : "fs-enable-login"
 | 
			
		||||
  ]);
 | 
			
		||||
  linkToShare = baseLink + QueryParameters.GetParts(new Set(excluded))
 | 
			
		||||
    .concat(excluded.map(k => k + "=" + false))
 | 
			
		||||
    .join("&");
 | 
			
		||||
  if (layout.definitionRaw !== undefined) {
 | 
			
		||||
    linkToShare += "&userlayout=" + (layout.definedAtUrl ?? layout.id);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async function shareCurrentLink() {
 | 
			
		||||
  await navigator.share({
 | 
			
		||||
    title: Translations.W(state.layout.title)?.ConstructElement().textContent ?? "MapComplete",
 | 
			
		||||
    text: Translations.W(state.layout.description)?.ConstructElement().textContent ?? "",
 | 
			
		||||
    url: linkToShare
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
let isCopied = false;
 | 
			
		||||
 | 
			
		||||
async function copyCurrentLink() {
 | 
			
		||||
  await navigator.clipboard.writeText(linkToShare);
 | 
			
		||||
  isCopied = true;
 | 
			
		||||
  await Utils.waitFor(5000);
 | 
			
		||||
  isCopied = false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
<div>
 | 
			
		||||
 | 
			
		||||
  <Tr t={tr.intro} />
 | 
			
		||||
  <div class="flex">
 | 
			
		||||
    {#if typeof navigator?.share === "function"}
 | 
			
		||||
      <button class="w-8 h-8 p-1 shrink-0" on:click={shareCurrentLink}>
 | 
			
		||||
        <ToSvelte construct={Svg.share_svg()} />
 | 
			
		||||
      </button>
 | 
			
		||||
    {/if}
 | 
			
		||||
    {#if navigator.clipboard !== undefined}
 | 
			
		||||
      <button class="w-8 h-8 p-1  shrink-0 no-image-background" on:click={copyCurrentLink}>
 | 
			
		||||
        <DocumentDuplicateIcon />
 | 
			
		||||
      </button>
 | 
			
		||||
    {/if}
 | 
			
		||||
    <div class="literal-code" on:click={e => Utils.selectTextIn(e.target)}>
 | 
			
		||||
      {linkToShare}
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div class="flex justify-center">
 | 
			
		||||
 | 
			
		||||
    {#if isCopied}
 | 
			
		||||
      <Tr t={tr.copiedToClipboard} cls="thanks m-2" />
 | 
			
		||||
    {/if}
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  <Tr t={ tr.embedIntro} />
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  <div class="flex flex-col my-1 link-underline">
 | 
			
		||||
 | 
			
		||||
    <label>
 | 
			
		||||
      <input bind:checked={showWelcomeMessage} type="checkbox" />
 | 
			
		||||
      <Tr t={tr.fsWelcomeMessage} />
 | 
			
		||||
    </label>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    <label>
 | 
			
		||||
      <input bind:checked={enableLogin} type="checkbox" />
 | 
			
		||||
      <Tr t={tr.fsUserbadge} />
 | 
			
		||||
    </label>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div class="literal-code m-1">
 | 
			
		||||
    <span class="literal-code iframe-code-block"> <br />
 | 
			
		||||
    <iframe src="${url}" <br />
 | 
			
		||||
    allow="geolocation" width="100%" height="100%" style="min-width: 250px; min-height: 250px" <br />
 | 
			
		||||
    title="${state.layout.title?.txt ?? "MapComplete"  } with MapComplete"> <br />
 | 
			
		||||
    </iframe> <br />
 | 
			
		||||
    </span>
 | 
			
		||||
  </div>
 | 
			
		||||
  <Tr t={tr.documentation} cls="link-underline"/>
 | 
			
		||||
</div>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,257 +0,0 @@
 | 
			
		|||
/* eslint-disable prefer-const */
 | 
			
		||||
import { VariableUiElement } from "../Base/VariableUIElement"
 | 
			
		||||
import { Translation } from "../i18n/Translation"
 | 
			
		||||
import Svg from "../../Svg"
 | 
			
		||||
import Combine from "../Base/Combine"
 | 
			
		||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
 | 
			
		||||
import { Utils } from "../../Utils"
 | 
			
		||||
import Translations from "../i18n/Translations"
 | 
			
		||||
import BaseUIElement from "../BaseUIElement"
 | 
			
		||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
 | 
			
		||||
import { InputElement } from "../Input/InputElement"
 | 
			
		||||
import { CheckBox } from "../Input/Checkboxes"
 | 
			
		||||
import { SubtleButton } from "../Base/SubtleButton"
 | 
			
		||||
import LZString from "lz-string"
 | 
			
		||||
import { SpecialVisualizationState } from "../SpecialVisualization"
 | 
			
		||||
 | 
			
		||||
export class ShareScreen extends Combine {
 | 
			
		||||
    constructor(state: SpecialVisualizationState) {
 | 
			
		||||
        const layout = state?.layout
 | 
			
		||||
        const tr = Translations.t.general.sharescreen
 | 
			
		||||
 | 
			
		||||
        const optionCheckboxes: InputElement<boolean>[] = []
 | 
			
		||||
        const optionParts: Store<string>[] = []
 | 
			
		||||
 | 
			
		||||
        const includeLocation = new CheckBox(tr.fsIncludeCurrentLocation, true)
 | 
			
		||||
        optionCheckboxes.push(includeLocation)
 | 
			
		||||
 | 
			
		||||
        const currentLocation = state.mapProperties.location
 | 
			
		||||
        const zoom = state.mapProperties.zoom
 | 
			
		||||
 | 
			
		||||
        optionParts.push(
 | 
			
		||||
            includeLocation.GetValue().map(
 | 
			
		||||
                (includeL) => {
 | 
			
		||||
                    if (currentLocation === undefined) {
 | 
			
		||||
                        return null
 | 
			
		||||
                    }
 | 
			
		||||
                    if (includeL) {
 | 
			
		||||
                        return [
 | 
			
		||||
                            ["z", zoom.data],
 | 
			
		||||
                            ["lat", currentLocation.data?.lat],
 | 
			
		||||
                            ["lon", currentLocation.data?.lon],
 | 
			
		||||
                        ]
 | 
			
		||||
                            .filter((p) => p[1] !== undefined)
 | 
			
		||||
                            .map((p) => p[0] + "=" + p[1])
 | 
			
		||||
                            .join("&")
 | 
			
		||||
                    } else {
 | 
			
		||||
                        return null
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
                [currentLocation, zoom]
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        function fLayerToParam(flayer: {
 | 
			
		||||
            isDisplayed: UIEventSource<boolean>
 | 
			
		||||
            layerDef: LayerConfig
 | 
			
		||||
        }) {
 | 
			
		||||
            if (flayer.isDisplayed.data) {
 | 
			
		||||
                return null // Being displayed is the default
 | 
			
		||||
            }
 | 
			
		||||
            return "layer-" + flayer.layerDef.id + "=" + flayer.isDisplayed.data
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const currentLayer: Store<
 | 
			
		||||
            { id: string; name: string | Record<string, string> } | undefined
 | 
			
		||||
        > = state.mapProperties.rasterLayer.map((l) => l?.properties)
 | 
			
		||||
        const currentBackground = new VariableUiElement(
 | 
			
		||||
            currentLayer.map((layer) => {
 | 
			
		||||
                return tr.fsIncludeCurrentBackgroundMap.Subs({ name: layer?.name ?? "" })
 | 
			
		||||
            })
 | 
			
		||||
        )
 | 
			
		||||
        const includeCurrentBackground = new CheckBox(currentBackground, true)
 | 
			
		||||
        optionCheckboxes.push(includeCurrentBackground)
 | 
			
		||||
        optionParts.push(
 | 
			
		||||
            includeCurrentBackground.GetValue().map(
 | 
			
		||||
                (includeBG) => {
 | 
			
		||||
                    if (includeBG) {
 | 
			
		||||
                        return "background=" + currentLayer.data?.id
 | 
			
		||||
                    } else {
 | 
			
		||||
                        return null
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
                [currentLayer]
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        const includeLayerChoices = new CheckBox(tr.fsIncludeCurrentLayers, true)
 | 
			
		||||
        optionCheckboxes.push(includeLayerChoices)
 | 
			
		||||
 | 
			
		||||
        optionParts.push(
 | 
			
		||||
            includeLayerChoices.GetValue().map(
 | 
			
		||||
                (includeLayerSelection) => {
 | 
			
		||||
                    if (includeLayerSelection) {
 | 
			
		||||
                        return Utils.NoNull(
 | 
			
		||||
                            Array.from(state.layerState.filteredLayers.values()).map(fLayerToParam)
 | 
			
		||||
                        ).join("&")
 | 
			
		||||
                    } else {
 | 
			
		||||
                        return null
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
                Array.from(state.layerState.filteredLayers.values()).map(
 | 
			
		||||
                    (flayer) => flayer.isDisplayed
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        const switches = [
 | 
			
		||||
            { urlName: "fs-userbadge", human: tr.fsUserbadge },
 | 
			
		||||
            { urlName: "fs-search", human: tr.fsSearch },
 | 
			
		||||
            { urlName: "fs-welcome-message", human: tr.fsWelcomeMessage },
 | 
			
		||||
            { urlName: "fs-layers", human: tr.fsLayers },
 | 
			
		||||
            { urlName: "fs-add-new", human: tr.fsAddNew },
 | 
			
		||||
            { urlName: "fs-geolocation", human: tr.fsGeolocation },
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        for (const swtch of switches) {
 | 
			
		||||
            const checkbox = new CheckBox(Translations.W(swtch.human))
 | 
			
		||||
            optionCheckboxes.push(checkbox)
 | 
			
		||||
            optionParts.push(
 | 
			
		||||
                checkbox.GetValue().map((isEn) => {
 | 
			
		||||
                    if (isEn) {
 | 
			
		||||
                        return null
 | 
			
		||||
                    } else {
 | 
			
		||||
                        return `${swtch.urlName}=false`
 | 
			
		||||
                    }
 | 
			
		||||
                })
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (layout.definitionRaw !== undefined) {
 | 
			
		||||
            optionParts.push(new UIEventSource("userlayout=" + (layout.definedAtUrl ?? layout.id)))
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const options = new Combine(optionCheckboxes).SetClass("flex flex-col")
 | 
			
		||||
        const url = (currentLocation ?? new UIEventSource(undefined)).map(() => {
 | 
			
		||||
            const host = window.location.host
 | 
			
		||||
            let path = window.location.pathname
 | 
			
		||||
            path = path.substr(0, path.lastIndexOf("/"))
 | 
			
		||||
            let id = layout.id.toLowerCase()
 | 
			
		||||
            if (layout.definitionRaw !== undefined) {
 | 
			
		||||
                id = "theme.html"
 | 
			
		||||
            }
 | 
			
		||||
            let literalText = `https://${host}${path}/${id}`
 | 
			
		||||
 | 
			
		||||
            let hash = ""
 | 
			
		||||
            if (layout.definedAtUrl === undefined && layout.definitionRaw !== undefined) {
 | 
			
		||||
                hash = "#" + LZString.compressToBase64(Utils.MinifyJSON(layout.definitionRaw))
 | 
			
		||||
            }
 | 
			
		||||
            const parts = Utils.NoEmpty(
 | 
			
		||||
                Utils.NoNull(optionParts.map((eventSource) => eventSource.data))
 | 
			
		||||
            )
 | 
			
		||||
            if (parts.length === 0) {
 | 
			
		||||
                return literalText + hash
 | 
			
		||||
            }
 | 
			
		||||
            return literalText + "?" + parts.join("&") + hash
 | 
			
		||||
        }, optionParts)
 | 
			
		||||
 | 
			
		||||
        const iframeCode = new VariableUiElement(
 | 
			
		||||
            url.map((url) => {
 | 
			
		||||
                return `<span class='literal-code iframe-code-block'>
 | 
			
		||||
                         <iframe src="${url}" allow="geolocation" width="100%" height="100%" style="min-width: 250px; min-height: 250px" title="${
 | 
			
		||||
                    layout.title?.txt ?? "MapComplete"
 | 
			
		||||
                } with MapComplete"></iframe>
 | 
			
		||||
                    </span>`
 | 
			
		||||
            })
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        const linkStatus = new UIEventSource<string | Translation>("")
 | 
			
		||||
        const link = new VariableUiElement(
 | 
			
		||||
            url.map(
 | 
			
		||||
                (url) =>
 | 
			
		||||
                    `<input type="text" value=" ${url}" id="code-link--copyable" style="width:90%">`
 | 
			
		||||
            )
 | 
			
		||||
        ).onClick(async () => {
 | 
			
		||||
            const shareData = {
 | 
			
		||||
                title: Translations.W(layout.title)?.ConstructElement().textContent ?? "",
 | 
			
		||||
                text: Translations.W(layout.description)?.ConstructElement().textContent ?? "",
 | 
			
		||||
                url: url.data,
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            function rejected() {
 | 
			
		||||
                const copyText = document.getElementById("code-link--copyable")
 | 
			
		||||
 | 
			
		||||
                // @ts-ignore
 | 
			
		||||
                copyText.select()
 | 
			
		||||
                // @ts-ignore
 | 
			
		||||
                copyText.setSelectionRange(0, 99999) /*For mobile devices*/
 | 
			
		||||
 | 
			
		||||
                document.execCommand("copy")
 | 
			
		||||
                const copied = tr.copiedToClipboard.Clone()
 | 
			
		||||
                copied.SetClass("thanks")
 | 
			
		||||
                linkStatus.setData(copied)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            try {
 | 
			
		||||
                navigator
 | 
			
		||||
                    .share(shareData)
 | 
			
		||||
                    .then(() => {
 | 
			
		||||
                        const thx = tr.thanksForSharing.Clone()
 | 
			
		||||
                        thx.SetClass("thanks")
 | 
			
		||||
                        linkStatus.setData(thx)
 | 
			
		||||
                    }, rejected)
 | 
			
		||||
                    .catch(rejected)
 | 
			
		||||
            } catch (err) {
 | 
			
		||||
                rejected()
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        let downloadThemeConfig: BaseUIElement = undefined
 | 
			
		||||
        if (layout.definitionRaw !== undefined) {
 | 
			
		||||
            const downloadThemeConfigAsJson = new SubtleButton(
 | 
			
		||||
                Svg.download_svg(),
 | 
			
		||||
                new Combine([tr.downloadCustomTheme, tr.downloadCustomThemeHelp.SetClass("subtle")])
 | 
			
		||||
                    .onClick(() => {
 | 
			
		||||
                        Utils.offerContentsAsDownloadableFile(
 | 
			
		||||
                            layout.definitionRaw,
 | 
			
		||||
                            layout.id + ".mapcomplete-theme-definition.json",
 | 
			
		||||
                            {
 | 
			
		||||
                                mimetype: "application/json",
 | 
			
		||||
                            }
 | 
			
		||||
                        )
 | 
			
		||||
                    })
 | 
			
		||||
                    .SetClass("flex flex-col")
 | 
			
		||||
            )
 | 
			
		||||
            let editThemeConfig: BaseUIElement = undefined
 | 
			
		||||
            if (layout.definedAtUrl === undefined) {
 | 
			
		||||
                const patchedDefinition = JSON.parse(layout.definitionRaw)
 | 
			
		||||
                patchedDefinition["language"] = Object.keys(patchedDefinition.title)
 | 
			
		||||
                editThemeConfig = new SubtleButton(
 | 
			
		||||
                    Svg.pencil_svg(),
 | 
			
		||||
                    "Edit this theme on the custom theme generator",
 | 
			
		||||
                    {
 | 
			
		||||
                        url: `https://pietervdvn.github.io/mc/legacy/070/customGenerator.html#${btoa(
 | 
			
		||||
                            JSON.stringify(patchedDefinition)
 | 
			
		||||
                        )}`,
 | 
			
		||||
                    }
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            downloadThemeConfig = new Combine([
 | 
			
		||||
                downloadThemeConfigAsJson,
 | 
			
		||||
                editThemeConfig,
 | 
			
		||||
            ]).SetClass("flex flex-col")
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        super([
 | 
			
		||||
            tr.intro,
 | 
			
		||||
            link,
 | 
			
		||||
            new VariableUiElement(linkStatus),
 | 
			
		||||
            downloadThemeConfig,
 | 
			
		||||
            tr.addToHomeScreen,
 | 
			
		||||
            tr.embedIntro,
 | 
			
		||||
            options,
 | 
			
		||||
            iframeCode,
 | 
			
		||||
        ])
 | 
			
		||||
        this.SetClass("flex flex-col link-underline")
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -7,7 +7,7 @@
 | 
			
		|||
  import type { LayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
 | 
			
		||||
  import Tr from "../Base/Tr.svelte"
 | 
			
		||||
  import SubtleLink from "../Base/SubtleLink.svelte"
 | 
			
		||||
    import Translations from "../i18n/Translations"
 | 
			
		||||
  import Translations from "../i18n/Translations"
 | 
			
		||||
 | 
			
		||||
  export let theme: LayoutInformation
 | 
			
		||||
  export let isCustom: boolean = false
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -45,9 +45,7 @@
 | 
			
		|||
    <Tr t={layout.description} />
 | 
			
		||||
    <Tr t={Translations.t.general.welcomeExplanation.general} />
 | 
			
		||||
    {#if layout.layers.some((l) => l.presets?.length > 0)}
 | 
			
		||||
      <If condition={state.featureSwitches.featureSwitchAddNew}>
 | 
			
		||||
        <Tr t={Translations.t.general.welcomeExplanation.addNew} />
 | 
			
		||||
      </If>
 | 
			
		||||
    {/if}
 | 
			
		||||
 | 
			
		||||
    <Tr t={layout.descriptionTail} />
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -29,7 +29,7 @@
 | 
			
		|||
    <div class="gap-4 md:grid md:grid-flow-row md:grid-cols-2 lg:grid-cols-3">
 | 
			
		||||
      {#each filteredThemes as theme (theme.id)}
 | 
			
		||||
        {#if theme !== undefined && !(hideThemes && theme?.hideFromOverview)}
 | 
			
		||||
        <!-- TODO: doesn't work if first theme is hidden -->
 | 
			
		||||
          <!-- TODO: doesn't work if first theme is hidden -->
 | 
			
		||||
          {#if theme === firstTheme && !isCustom && $search !== "" && $search !== undefined}
 | 
			
		||||
            <ThemeButton
 | 
			
		||||
              {theme}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -37,6 +37,7 @@
 | 
			
		|||
      <a
 | 
			
		||||
        href={osmConnection.Backend() + "/profile/edit"}
 | 
			
		||||
        target="_blank"
 | 
			
		||||
        rel="noopener"
 | 
			
		||||
        class="link-no-underline flex items-center self-end"
 | 
			
		||||
      >
 | 
			
		||||
        <PencilAltIcon slot="image" class="h-8 w-8 p-2" />
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue