diff --git a/.changeset/twelve-snakes-hammer.md b/.changeset/twelve-snakes-hammer.md new file mode 100644 index 0000000000..f6d9830140 --- /dev/null +++ b/.changeset/twelve-snakes-hammer.md @@ -0,0 +1,5 @@ +--- +"@undp-data/svelte-undp-design": patch +--- + +added Download component in svelte-undp-design diff --git a/packages/svelte-undp-design/package.json b/packages/svelte-undp-design/package.json index 61747c5d42..231f315289 100644 --- a/packages/svelte-undp-design/package.json +++ b/packages/svelte-undp-design/package.json @@ -59,6 +59,7 @@ }, "type": "module", "dependencies": { + "filesize": "^10.0.6", "svelte": "^3.55.0", "svelte-carousel": "^1.0.20" }, diff --git a/packages/svelte-undp-design/src/lib/Download/Download.stories.ts b/packages/svelte-undp-design/src/lib/Download/Download.stories.ts new file mode 100644 index 0000000000..90aabee435 --- /dev/null +++ b/packages/svelte-undp-design/src/lib/Download/Download.stories.ts @@ -0,0 +1,32 @@ +import type { Meta, StoryObj } from '@storybook/svelte'; + +import Download from './Download.svelte'; + +// More on how to set up stories at: https://storybook.js.org/docs/7.0/svelte/writing-stories/introduction +const meta = { + title: 'Example/Download', + component: Download, + tags: ['docsPage'], + argTypes: { + url: { + type: 'string', + description: 'URL for doanloaded file' + }, + title: { + type: 'string', + description: 'Title for file', + defaultValue: '' + } + } +} satisfies Meta<Download>; + +export default meta; +type Story = StoryObj<typeof meta>; + +// More on writing stories with args: https://storybook.js.org/docs/7.0/svelte/writing-stories/args +export const Primary: Story = { + args: { + url: 'assets/undp-logo-blue.svg', + title: 'undp-logo-blue.svg' + } +}; diff --git a/packages/svelte-undp-design/src/lib/Download/Download.svelte b/packages/svelte-undp-design/src/lib/Download/Download.svelte new file mode 100644 index 0000000000..0f57f5b0d9 --- /dev/null +++ b/packages/svelte-undp-design/src/lib/Download/Download.svelte @@ -0,0 +1,89 @@ +<script lang="ts"> + import { filesize } from 'filesize'; + + export let url: string; + export let title = ''; + let extension = ''; + + const downloadFile = () => { + const element = document.createElement('a'); + element.href = url; + element.download = url; + element.click(); + element.remove(); + }; + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Enter') { + downloadFile(); + } + }; + + const fileUrl = new URL(url); + const filePath = fileUrl.pathname.split('/'); + const fileName = filePath[filePath.length - 1]; + if (!title) { + title = fileName; + } + const fileExtensions = fileName.split('.'); + extension = ''; + if (fileExtensions.length > 1) { + extension = fileExtensions[fileExtensions.length - 1].toLocaleUpperCase(); + } + + let fileFormat = extension; + + const getFileSize = () => { + return new Promise<void>((resolve, reject) => { + const fileUrl = new URL(url.replace('pmtiles://', '')); + const filePath = fileUrl.pathname.split('/'); + let bytes = 'N/A'; + fetch(fileUrl.toString()).then((res) => { + if (res.ok) { + const contentLength = res.headers.get('content-length'); + if (contentLength) { + bytes = filesize(Number(contentLength), { round: 1 }) as string; + } + } + fileFormat = `${fileFormat} (${bytes})`; + resolve(); + }); + }); + }; + + $: if (url) { + getFileSize(); + } +</script> + +<div class="download-card"> + <!-- svelte-ignore a11y-missing-attribute --> + <a role="button" on:click={downloadFile} on:keydown={handleKeyDown}> + <div class="description"> + {#if title} + <p class="title">{title}</p> + {/if} + <p class="format"> + {fileFormat} + </p> + <span class="download"> + Download + <span class="download-animated"><i /></span> + </span> + </div> + </a> +</div> + +<style lang="scss"> + @use '../css/base-minimal.min.css'; + @use '../css/download-card.min.css'; + + .download-card { + cursor: pointer; + + .description { + width: fit-content; + max-width: 300px; + } + } +</style> diff --git a/packages/svelte-undp-design/src/lib/css/download-card.min.css b/packages/svelte-undp-design/src/lib/css/download-card.min.css new file mode 100644 index 0000000000..051e7adb19 --- /dev/null +++ b/packages/svelte-undp-design/src/lib/css/download-card.min.css @@ -0,0 +1,179 @@ +div.download-card .download, +div.download-card .format { + font-size: 0.875rem; + line-height: 1.4; +} +@media (min-width: 48em) { + div.download-card .download, + div.download-card .format { + font-size: 1rem; + } +} +div.download-card .title { + font-size: 1rem; +} +@media (min-width: 48em) { + div.download-card .title { + font-size: 1.25rem; + } +} +:lang(my) div.download-card .download, +:lang(my) div.download-card .format, +div.download-card :lang(my) .download, +div.download-card :lang(my) .format { + font-size: 0.75rem; + line-height: 1.7; +} +@media (min-width: 48em) { + :lang(my) div.download-card .download, + :lang(my) div.download-card .format, + div.download-card :lang(my) .download, + div.download-card :lang(my) .format { + font-size: 0.875rem; + } +} +:lang(my) div.download-card .title, +div.download-card :lang(my) .title { + font-size: 0.875rem; +} +@media (min-width: 48em) { + :lang(my) div.download-card .title, + div.download-card :lang(my) .title { + font-size: 1rem; + } +} +div.download-card { + display: inline-block; +} +@media (min-width: 48em) { + div.download-card { + min-width: 330px; + } +} +div.download-card > a { + background: none; +} +div.download-card:hover div.publication-thumbnail__image.yellow:after, +div.download-card:hover div.publication-thumbnail__image:after { + background: linear-gradient(27.66deg, #ffeb00, transparent 70.49%); + opacity: 0.75; +} +div.download-card:hover div.publication-thumbnail__image.red:after { + background: linear-gradient(27.66deg, #ee402d, transparent 70.49%); + opacity: 0.75; +} +div.download-card:hover div.publication-thumbnail__image.green:after { + background: linear-gradient(27.66deg, #6de354, transparent 70.49%); + opacity: 0.75; +} +div.download-card:hover div.publication-thumbnail__image.blue:after { + background: linear-gradient(27.66deg, #60d4f2, transparent 70.49%); + opacity: 0.75; +} +div.download-card:hover div.card-thumbnail__image.yellow:before, +div.download-card:hover div.card-thumbnail__image:before { + background: linear-gradient(67.76deg, #ffeb00, transparent 61.11%); + opacity: 0.75; +} +div.download-card:hover div.card-thumbnail__image.red:before { + background: linear-gradient(67.76deg, #ee402d, transparent 61.11%); + opacity: 0.75; +} +div.download-card:hover div.card-thumbnail__image.green:before { + background: linear-gradient(67.76deg, #6de354, transparent 61.11%); + opacity: 0.75; +} +div.download-card:hover div.card-thumbnail__image.blue:before { + background: linear-gradient(67.76deg, #60d4f2, transparent 61.11%); + opacity: 0.75; +} +div.download-card:hover .download-animated:after { + -webkit-transform: rotate(-45deg) translate(7px, -7px); + -moz-transform: rotate(-45deg) translate(7px, -7px); + -ms-transform: rotate(-45deg) translate(7px, -7px); + -o-transform: rotate(-45deg) translate(7px, -7px); + transition: rotate(-45deg) translate(7px, -7px); +} +div.download-card:hover .download-animated:before { + -webkit-transform: translateY(-10px); + -moz-transform: translateY(-10px); + -ms-transform: translateY(-10px); + -o-transform: translateY(-10px); + transition: translate(0, -10px); +} +div.download-card:hover .external-link-animated:after { + -webkit-transform: translate(5px, -5px); + -moz-transform: translate(5px, -5px); + -ms-transform: translate(5px, -5px); + -o-transform: translate(5px, -5px); + transition: translate(5px, -5px); +} +div.download-card:hover .external-link-animated:before { + -webkit-transform: rotate(-45deg) translate(7px); + -moz-transform: rotate(-45deg) translate(7px); + -ms-transform: rotate(-45deg) translate(7px); + -o-transform: rotate(-45deg) translate(7px); + transition: rotate(-45deg) translate(7px, 0); +} +div.download-card .description { + background: #fafafa; + padding: 1.5rem; +} +div.download-card .title { + margin-bottom: 0.25rem; +} +div.download-card .format { + color: #55606e; + margin-bottom: 1rem; +} +div.download-card .download { + align-items: center; + background-image: none; + display: flex; + font-weight: 700; + letter-spacing: 0.03em; + text-transform: uppercase; +} +div.download-card .download .download-animated { + margin-left: 0.75rem; +} +div.download-card .download .external-link-animated { + margin-bottom: 0.5rem; + margin-left: 0.75rem; +} +[dir='rtl'] div.download-card .download-animated, +[dir='rtl'] div.download-card .external-link-animated { + margin-left: 0; + margin-right: 0.75rem; +} +[dir='rtl'] div.download-card:hover div.publication-thumbnail__image.yellow:after, +[dir='rtl'] div.download-card:hover div.publication-thumbnail__image:after { + background: linear-gradient(318deg, #ffeb00, transparent 70.49%); +} +[dir='rtl'] div.download-card:hover div.publication-thumbnail__image.red:after { + background: linear-gradient(318deg, #ee402d, transparent 70.49%); +} +[dir='rtl'] div.download-card:hover div.publication-thumbnail__image.green:after { + background: linear-gradient(318deg, #6de354, transparent 70.49%); +} +[dir='rtl'] div.download-card:hover div.publication-thumbnail__image.blue:after { + background: linear-gradient(318deg, #60d4f2, transparent 70.49%); +} +[dir='rtl'] div.download-card:hover div.card-thumbnail__image.yellow:before, +[dir='rtl'] div.download-card:hover div.card-thumbnail__image:before { + background: linear-gradient(297deg, #ffeb00, transparent 61.11%); +} +[dir='rtl'] div.download-card:hover div.card-thumbnail__image.red:before { + background: linear-gradient(297deg, #ee402d, transparent 61.11%); +} +[dir='rtl'] div.download-card:hover div.card-thumbnail__image.green:before { + background: linear-gradient(297deg, #6de354, transparent 61.11%); +} +[dir='rtl'] div.download-card:hover div.card-thumbnail__image.blue:before { + background: linear-gradient(297deg, #60d4f2, transparent 61.11%); +} +[dir='rtl'] div.download-card div.download-card__download .download-animated, +[dir='rtl'] div.download-card div.download-card__download:after { + margin-left: 0; + margin-right: 0.75rem; +} diff --git a/packages/svelte-undp-design/src/lib/index.ts b/packages/svelte-undp-design/src/lib/index.ts index e9c450f23e..e0a351f31e 100644 --- a/packages/svelte-undp-design/src/lib/index.ts +++ b/packages/svelte-undp-design/src/lib/index.ts @@ -11,6 +11,7 @@ import Radios from './Radios/Radios.svelte'; import Loader from './Loader/Loader.svelte'; import Pagination from './Pagination/Pagination.svelte'; import FluidCarousel from './FluidCarousel/FluidCarousel.svelte'; +import Download from './Download/Download.svelte'; export { Accordion, @@ -18,6 +19,7 @@ export { Breadcrumbs, Checkbox, CardWithImage, + Download, Header, Footer, Tabs, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f087de17bf..cf3a39d12f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -265,6 +265,7 @@ importers: eslint-config-prettier: ^8.6.0 eslint-plugin-storybook: ^0.6.9 eslint-plugin-svelte3: ^4.0.0 + filesize: ^10.0.6 prettier: ^2.8.2 prettier-plugin-svelte: ^2.9.0 react: ^18.2.0 @@ -279,6 +280,7 @@ importers: typescript: ^4.9.4 vite: ^4.0.4 dependencies: + filesize: 10.0.6 svelte: 3.55.0 svelte-carousel: 1.0.20 devDependencies: @@ -6805,6 +6807,11 @@ packages: minimatch: 5.1.2 dev: true + /filesize/10.0.6: + resolution: {integrity: sha512-rzpOZ4C9vMFDqOa6dNpog92CoLYjD79dnjLk2TYDDtImRIyLTOzqojCb05Opd1WuiWjs+fshhCgTd8cl7y5t+g==} + engines: {node: '>= 10.4.0'} + dev: false + /fill-range/4.0.0: resolution: {integrity: sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==} engines: {node: '>=0.10.0'} diff --git a/sites/geohub/src/components/controls/DataCardInfoButton.svelte b/sites/geohub/src/components/controls/DataCardInfoButton.svelte index 6371448cb0..0560a83c13 100644 --- a/sites/geohub/src/components/controls/DataCardInfoButton.svelte +++ b/sites/geohub/src/components/controls/DataCardInfoButton.svelte @@ -50,9 +50,9 @@ use:popperContent={popperOptions} use:clickOutside={() => (isPopupShown = false)} transition:fade> + <!-- svelte-ignore a11y-click-events-have-key-events --> <div class="close" - alt="Close" title="Close" on:click={() => (isPopupShown = false)}> <i class="fa-solid fa-xmark sm" /> @@ -75,7 +75,7 @@ @import '../../styles/popper.scss'; #tooltip { - max-width: 300px; + max-width: 330px; inset: -10px auto auto 0px !important; .close { @@ -91,6 +91,7 @@ font-weight: lighter; max-height: 300px; overflow-y: auto; + overflow-x: hidden; } } </style> diff --git a/sites/geohub/src/components/data-view/DataCardInfo.svelte b/sites/geohub/src/components/data-view/DataCardInfo.svelte index e166f589d3..789b27d3e0 100644 --- a/sites/geohub/src/components/data-view/DataCardInfo.svelte +++ b/sites/geohub/src/components/data-view/DataCardInfo.svelte @@ -2,7 +2,7 @@ import { marked } from 'marked' import Time from 'svelte-time' import type { RasterTileMetadata, StacItemFeature, VectorTileMetadata } from '$lib/types' - import { CtaLink } from '@undp-data/svelte-undp-design' + import { CtaLink, Download } from '@undp-data/svelte-undp-design' import { MAP_ATTRIBUTION } from '$lib/constants' export let feature: StacItemFeature = undefined @@ -10,6 +10,14 @@ const is_raster: boolean = feature.properties.is_raster as unknown as boolean + const tags: [{ key: string; value: string }] = feature.properties.tags as unknown as [{ key: string; value: string }] + const stacType = tags?.find((tag) => tag.key === 'stac') + + const url = feature.properties.url + + const isStac = is_raster && stacType ? true : false + const isPbf = !is_raster && url.toLocaleLowerCase().endsWith('.pbf') + let attribution = MAP_ATTRIBUTION if (feature.properties.source) { attribution = feature.properties.source @@ -25,6 +33,21 @@ let isFullDescription = false let descriptionLength = 100 + + interface FileOptions { + title: string + url: string + } + + let file: FileOptions + if (!(isStac === true || isPbf === true)) { + const fileUrl = new URL(url.replace('pmtiles://', '')) + const filePath = fileUrl.pathname.split('/') + file = { + title: filePath[filePath.length - 1], + url: fileUrl.toString(), + } + } </script> <div class="container"> @@ -83,6 +106,11 @@ timestamp={feature.properties.updatedat} format="h:mm A, MMMM D, YYYY" /> </p> + {#if file} + <Download + title={file.title} + url={file.url} /> + {/if} {/if} </div> {/if}