feat(demo): the welcome dialog credits every photo of the demo content

A Photo credits link in the dialog's footer opens a second view with every photo: its title linking to its page on Wikimedia Commons, its author and its license linking to the deed. The list is its own chunk, loaded when a visitor opens it. The countdown test reads the clock the interval moved.
This commit is contained in:
Maurice
2026-10-24 23:22:10 +00:00
parent 5596355141
commit a9fb0fc475
35 changed files with 1095 additions and 70 deletions
@@ -0,0 +1,76 @@
import { useEffect, useRef } from 'react';
import { useTranslation } from '../../i18n';
import { fs } from '../shared/DialogShell';
import {
PHOTO_CREDITS,
PUBLIC_DOMAIN,
commonsPage,
creditTitle,
licenseDeed,
type PhotoCredit,
} from './demoCreditsModel';
type Translate = ReturnType<typeof useTranslation>['t'];
const LINK = 'underline decoration-edge underline-offset-2 hover:text-content';
/** One photo: its title linking to its page on Commons, who took it, and its license linking to the deed. */
function CreditRow({ credit, t }: { credit: PhotoCredit; t: Translate }) {
const deed = licenseDeed(credit.license);
const license = credit.license === PUBLIC_DOMAIN ? t('demo.credits.publicDomain') : credit.license;
return (
<li className="flex flex-col gap-0.5 py-2.5">
<a
href={commonsPage(credit.file)}
target="_blank"
rel="noopener noreferrer"
className={`${LINK} self-start break-words font-medium text-content`}
style={fs(12.5, 'body')}
>
{creditTitle(credit.file)}
</a>
<span className="flex flex-wrap gap-x-3 gap-y-0.5 text-content-muted" style={fs(12, 'body')}>
<span>{t('demo.credits.by', { author: credit.author })}</span>
{deed ? (
<a href={deed} target="_blank" rel="noopener noreferrer" className={LINK}>
{license}
</a>
) : (
<span>{license}</span>
)}
</span>
</li>
);
}
/**
* The credits for the demo's photos, the welcome dialog's second view. Its own
* chunk, loaded when a visitor opens it: only a demo instance shows the
* welcome, and few visitors open its credits.
*/
export default function DemoCredits() {
const { t } = useTranslation();
const intro = useRef<HTMLParagraphElement>(null);
// The dialog's body keeps the welcome's scroll position when the credits
// take its place; they start at their top.
useEffect(() => {
intro.current?.scrollIntoView({ block: 'nearest' });
}, []);
return (
<>
<p ref={intro} className="m-0 scroll-mt-5 leading-snug text-content-secondary" style={fs(12.5, 'body')}>
{t('demo.credits.intro')}
</p>
<ul
aria-label={t('demo.credits.title')}
className="m-0 list-none divide-y divide-edge-faint rounded-[16px] border border-edge-faint bg-surface-card px-3.5"
>
{PHOTO_CREDITS.map((credit) => (
<CreditRow key={credit.file} credit={credit} t={t} />
))}
</ul>
</>
);
}
@@ -1,4 +1,4 @@
import { act, fireEvent } from '@testing-library/react';
import { act, fireEvent, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { render, screen } from '../../../tests/helpers/render';
@@ -66,13 +66,14 @@ describe('DemoWelcome', () => {
it('FE-COMP-DEMOWELCOME-005: the countdown runs live in the server zone', async () => {
vi.useFakeTimers({ shouldAdvanceTime: false });
// On a whole second, so every tick of the interval lands on one too.
vi.setSystemTime(new Date(Date.UTC(2026, 9, 24, 12, 17, 53)));
render(<DemoWelcome />);
expect(screen.getByTestId('demo-countdown')).toHaveTextContent('42:07');
// Advancing the fake timers moves the fake clock with them: the fifth tick reads 12:17:58.
await act(async () => {
vi.setSystemTime(new Date(Date.UTC(2026, 9, 24, 12, 17, 58)));
vi.advanceTimersByTime(1000);
vi.advanceTimersByTime(5000);
});
expect(screen.getByTestId('demo-countdown')).toHaveTextContent('42:02');
});
@@ -89,4 +90,60 @@ describe('DemoWelcome', () => {
fireEvent.keyDown(document, { key: 'Escape' });
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});
it('FE-COMP-DEMOWELCOME-008: the photo credits open in its place and the back button returns', async () => {
const user = userEvent.setup();
render(<DemoWelcome />);
await user.click(screen.getByRole('button', { name: 'Photo credits' }));
const list = await screen.findByRole('list', { name: 'Photo credits' }, { timeout: 5000 });
expect(within(list).getAllByRole('listitem')).toHaveLength(228);
expect(screen.getByText(/come from Wikimedia Commons/)).toBeInTheDocument();
expect(screen.getByRole('heading', { name: 'Photo credits' })).toBeInTheDocument();
expect(screen.queryByText('Everything resets every hour')).not.toBeInTheDocument();
expect(screen.getByRole('dialog', { name: 'Live demo' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Back' })).toHaveFocus();
await user.click(screen.getByRole('button', { name: 'Back' }));
expect(screen.getByText('Everything resets every hour')).toBeInTheDocument();
expect(screen.queryByRole('list', { name: 'Photo credits' })).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Photo credits' })).toHaveFocus();
});
it('FE-COMP-DEMOWELCOME-009: a photo names its title, author and license, each linking to its source', async () => {
const user = userEvent.setup();
render(<DemoWelcome />);
await user.click(screen.getByRole('button', { name: 'Photo credits' }));
const title = await screen.findByRole('link', { name: 'Lagoa Rasa (Sete Cidades) 2' }, { timeout: 5000 });
expect(title).toHaveAttribute('href', 'https://commons.wikimedia.org/wiki/File:Lagoa_Rasa_(Sete_Cidades)_2.jpg');
expect(title).toHaveAttribute('target', '_blank');
expect(title).toHaveAttribute('rel', 'noopener noreferrer');
const row = within(title.closest('li') as HTMLElement);
expect(row.getByText('by Walmaul')).toBeInTheDocument();
const license = row.getByRole('link', { name: 'CC BY-SA 4.0' });
expect(license).toHaveAttribute('href', 'https://creativecommons.org/licenses/by-sa/4.0/');
expect(license).toHaveAttribute('target', '_blank');
expect(license).toHaveAttribute('rel', 'noopener noreferrer');
});
it('FE-COMP-DEMOWELCOME-010: a public domain photo names it without a deed, and a cleaned author reads as a name', async () => {
const user = userEvent.setup();
render(<DemoWelcome />);
await user.click(screen.getByRole('button', { name: 'Photo credits' }));
const abiskoTitle = await screen.findByRole('link', { name: 'AbiskoNationalpark01' }, { timeout: 5000 });
const abisko = within(abiskoTitle.closest('li') as HTMLElement);
expect(abisko.getByText('Public domain')).toBeInTheDocument();
expect(abisko.getAllByRole('link')).toHaveLength(1);
const zinnen = within(
screen.getByRole('link', { name: 'Drei Zinnen Tre Cime di Lavaredo Dolomites' }).closest('li') as HTMLElement
);
expect(zinnen.getByText('by Wolfgang Moroder')).toBeInTheDocument();
expect(zinnen.getByRole('link', { name: 'CC BY-SA 3.0' })).toHaveAttribute(
'href',
'https://creativecommons.org/licenses/by-sa/3.0/'
);
});
});
+121 -63
View File
@@ -1,5 +1,5 @@
import { ArrowRight, Clock, ExternalLink, RotateCcw } from 'lucide-react';
import { useId } from 'react';
import { ArrowLeft, ArrowRight, Clock, ExternalLink, Images, RotateCcw } from 'lucide-react';
import { lazy, Suspense, useId } from 'react';
import { useTranslation } from '../../i18n';
import {
DialogButton,
@@ -9,16 +9,22 @@ import {
DialogShell,
DialogTile,
FooterSpacer,
fs,
NEUTRAL_TINT,
PILL,
fs,
} from '../shared/DialogShell';
import ErrorBoundary from '../shared/ErrorBoundary';
import TrekIcon from '../shared/TrekIcon';
import { DEMO_FEATURES, DEMO_FULL_VERSION, DEMO_LIMITS, TREK_REPO_URL, type DemoLine } from './demoWelcomeModel';
import { useDemoWelcome } from './useDemoWelcome';
// The credits and their photo list load when a visitor opens them, not with the dashboard.
const DemoCredits = lazy(() => import('./DemoCredits'));
type Translate = ReturnType<typeof useTranslation>['t'];
const FOOTER_LINK = 'inline-flex items-center gap-1.5 font-medium text-content-muted no-underline hover:text-content';
/** The tiles: what a visitor can open right now, one card each. */
function FeatureGrid({ t }: { t: Translate }) {
return (
@@ -57,67 +63,10 @@ function LineList({ lines, tone, t }: { lines: readonly DemoLine[]; tone: 'muted
);
}
/**
* The welcome a demo instance opens with: what TREK is, that the demo resets
* on the hour (with the live countdown), what to try right away, what the
* demo leaves out and what the full version adds. Closed by the button,
* Escape or a click on the backdrop, and shown again on the next visit.
*/
export default function DemoWelcome() {
const { t } = useTranslation();
const labelId = useId();
const { open, close, countdown } = useDemoWelcome();
/** The welcome itself: the hourly reset, the tiles, and the two lists under them. */
function WelcomeBody({ t }: { t: Translate }) {
return (
<DialogShell
open={open}
onClose={close}
labelledBy={labelId}
width="wide"
header={
<DialogHeader
tile={
<DialogTile>
<TrekIcon className="h-7 w-7 text-content" />
</DialogTile>
}
tint={NEUTRAL_TINT}
labelId={labelId}
onClose={close}
eyebrow={t('demo.welcome.eyebrow')}
title={t('demo.welcome.title')}
sub={t('demo.welcome.tagline')}
subWraps
pills={
<span className={PILL}>
<Clock size={13} strokeWidth={2.2} aria-hidden />
{t('demo.welcome.resetIn')}
<span className="font-geist tabular-nums" data-testid="demo-countdown">
{countdown}
</span>
</span>
}
/>
}
footer={
<DialogFooter>
<a
href={TREK_REPO_URL}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1.5 font-medium text-content-muted no-underline hover:text-content"
style={fs(12.5, 'body')}
>
<ExternalLink size={15} strokeWidth={2} aria-hidden />
{t('demo.welcome.repo')}
</a>
<FooterSpacer />
<DialogButton variant="primary" onClick={close} autoFocus icon={<ArrowRight size={15} strokeWidth={2.2} />}>
{t('demo.welcome.start')}
</DialogButton>
</DialogFooter>
}
>
<>
<div className="flex items-start gap-3 rounded-[16px] bg-warning-soft px-4 py-3">
<RotateCcw size={17} strokeWidth={2.2} className="mt-0.5 flex-none text-warning" aria-hidden />
<div className="min-w-0">
@@ -142,6 +91,115 @@ export default function DemoWelcome() {
<LineList lines={DEMO_FULL_VERSION} tone="accent" t={t} />
</DialogSection>
</div>
</>
);
}
/**
* The footer's links: the source code, and the credits for the demo's photos.
* One above the other on a phone, where the footer has no room for both
* beside the button.
*/
function FooterLinks({ t, onCredits, focusCredits }: { t: Translate; onCredits: () => void; focusCredits: boolean }) {
return (
<div className="flex min-w-0 flex-col items-start gap-1.5 sm:flex-row sm:items-center sm:gap-4">
<a
href={TREK_REPO_URL}
target="_blank"
rel="noopener noreferrer"
className={FOOTER_LINK}
style={fs(12.5, 'body')}
>
<ExternalLink size={15} strokeWidth={2} aria-hidden />
{t('demo.welcome.repo')}
</a>
<button
type="button"
onClick={onCredits}
autoFocus={focusCredits}
className={FOOTER_LINK}
style={fs(12.5, 'body')}
>
<Images size={15} strokeWidth={2} aria-hidden />
{t('demo.welcome.credits')}
</button>
</div>
);
}
/**
* The welcome a demo instance opens with: what TREK is, that the demo resets
* on the hour (with the live countdown), what to try right away, what the
* demo leaves out and what the full version adds. The photo credits open in
* its place and back. Closed by the button, Escape or a click on the
* backdrop, and shown again on the next visit.
*/
export default function DemoWelcome() {
const { t } = useTranslation();
const labelId = useId();
const { open, close, countdown, creditsOpen, backFromCredits, openCredits, closeCredits } = useDemoWelcome();
return (
<DialogShell
open={open}
onClose={close}
labelledBy={labelId}
width="wide"
header={
<DialogHeader
tile={
<DialogTile>
<TrekIcon className="h-7 w-7 text-content" />
</DialogTile>
}
tint={NEUTRAL_TINT}
labelId={labelId}
onClose={close}
eyebrow={t('demo.welcome.eyebrow')}
title={t(creditsOpen ? 'demo.credits.title' : 'demo.welcome.title')}
sub={creditsOpen ? undefined : t('demo.welcome.tagline')}
subWraps
pills={
<span className={PILL}>
<Clock size={13} strokeWidth={2.2} aria-hidden />
{t('demo.welcome.resetIn')}
<span className="font-geist tabular-nums" data-testid="demo-countdown">
{countdown}
</span>
</span>
}
/>
}
footer={
<DialogFooter>
{/* The start button keeps its place in both views, so switching does not move the focus onto it again. */}
{creditsOpen ? (
<DialogButton
onClick={closeCredits}
autoFocus
icon={<ArrowLeft size={15} strokeWidth={2.2} className="rtl:-scale-x-100" />}
>
{t('demo.credits.back')}
</DialogButton>
) : (
<FooterLinks t={t} onCredits={openCredits} focusCredits={backFromCredits} />
)}
<FooterSpacer />
<DialogButton variant="primary" onClick={close} autoFocus icon={<ArrowRight size={15} strokeWidth={2.2} />}>
{t('demo.welcome.start')}
</DialogButton>
</DialogFooter>
}
>
{creditsOpen ? (
<ErrorBoundary boundaryId="demo-credits">
<Suspense fallback={null}>
<DemoCredits />
</Suspense>
</ErrorBoundary>
) : (
<WelcomeBody t={t} />
)}
</DialogShell>
);
}
@@ -0,0 +1,54 @@
import { describe, expect, it } from 'vitest';
import { PHOTO_CREDITS, PUBLIC_DOMAIN, commonsPage, creditTitle, licenseDeed } from './demoCreditsModel';
describe('demo photo credits', () => {
it('FE-DEMO-CREDITS-001: credits every photo of the demo content, each file once', () => {
expect(PHOTO_CREDITS).toHaveLength(228);
expect(new Set(PHOTO_CREDITS.map((c) => c.file)).size).toBe(228);
for (const { file } of PHOTO_CREDITS) expect(file, file).toMatch(/\.jpe?g$/i);
});
it('FE-DEMO-CREDITS-002: every author is a plain name, with no Commons notice or link label left', () => {
for (const { file, author } of PHOTO_CREDITS) {
expect(author.trim(), file).not.toBe('');
expect(author, file).toBe(author.trim());
expect(author.length, `${file}: ${author}`).toBeLessThanOrEqual(60);
expect(author, file).not.toMatch(/\s{2,}|\n/);
expect(author, file).not.toMatch(
/Photo was taken|machine-readable|assumed|Feel free|copyright|https?:|User:|\((?:Website|Flickr)\)/i
);
}
const authorOf = (file: string) => PHOTO_CREDITS.find((c) => c.file === file)?.author;
expect(authorOf('Drei Zinnen Tre Cime di Lavaredo Dolomites.jpg')).toBe('Wolfgang Moroder');
expect(authorOf('Iceland Godafoss 1972.jpg')).toBe('Roger McLassus');
expect(authorOf('Arashiyama Bamboo Grove (Unsplash).jpg')).toBe('Erol Ahmed');
});
it('FE-DEMO-CREDITS-003: every license has its deed, except the public domain', () => {
for (const { file, license } of PHOTO_CREDITS) {
if (license === PUBLIC_DOMAIN) expect(licenseDeed(license), file).toBeUndefined();
else expect(licenseDeed(license), `${file}: ${license}`).toMatch(/^https:\/\/creativecommons\.org\/.+\/$/);
}
});
it('FE-DEMO-CREDITS-004: derives the deed from the license name', () => {
expect(licenseDeed('CC BY-SA 4.0')).toBe('https://creativecommons.org/licenses/by-sa/4.0/');
expect(licenseDeed('CC BY 2.5')).toBe('https://creativecommons.org/licenses/by/2.5/');
expect(licenseDeed('CC BY-SA 2.0 de')).toBe('https://creativecommons.org/licenses/by-sa/2.0/de/');
expect(licenseDeed('CC0')).toBe('https://creativecommons.org/publicdomain/zero/1.0/');
expect(licenseDeed(PUBLIC_DOMAIN)).toBeUndefined();
expect(licenseDeed('GFDL')).toBeUndefined();
});
it('FE-DEMO-CREDITS-005: shows the title without its extension and links the page on Commons', () => {
expect(creditTitle('Café A Brasileira. Sculpture.jpg')).toBe('Café A Brasileira. Sculpture');
expect(creditTitle('Svínafellsjökull.JPG')).toBe('Svínafellsjökull');
expect(creditTitle('No extension')).toBe('No extension');
expect(commonsPage('Lagoa Rasa (Sete Cidades) 2.jpg')).toBe(
'https://commons.wikimedia.org/wiki/File:Lagoa_Rasa_(Sete_Cidades)_2.jpg'
);
expect(commonsPage('Glacier du Géant & Mont Blanc.jpg')).toBe(
'https://commons.wikimedia.org/wiki/File:Glacier_du_G%C3%A9ant_%26_Mont_Blanc.jpg'
);
});
});
@@ -0,0 +1,50 @@
import credits from './photoCredits.json';
/**
* One photo of the demo content as Wikimedia Commons credits it: the file's
* name there, its author and the short name of its license. The page on
* Commons, the title to show and the license's deed all follow from these, so
* photoCredits.json holds nothing else.
*/
export interface PhotoCredit {
file: string;
author: string;
license: string;
}
/**
* Every photo in the demo content, sorted by file name. The authors are the
* plain names: where Commons fills the field with a notice or a link label
* ("This Photo was taken by ...", "(Flickr)"), the list holds the name it
* gives, and where the author asks to be credited in a set form, that form.
*/
export const PHOTO_CREDITS: readonly PhotoCredit[] = credits;
/** The license name Commons gives a photo nobody holds a copyright in; it has no deed to link. */
export const PUBLIC_DOMAIN = 'Public domain';
const COMMONS_FILE_PAGE = 'https://commons.wikimedia.org/wiki/File:';
/** The file's name without its extension, the title Commons shows above the photo. */
export function creditTitle(file: string): string {
const dot = file.lastIndexOf('.');
return dot > 0 ? file.slice(0, dot) : file;
}
/** The file's page on Commons, with its full description and license. */
export function commonsPage(file: string): string {
return COMMONS_FILE_PAGE + encodeURIComponent(file.replace(/ /g, '_'));
}
/**
* The deed of a Creative Commons license by the short name Commons gives it:
* "CC BY-SA 4.0", a ported one such as "CC BY-SA 2.0 de", or "CC0". Nothing
* for the public domain, which is no license.
*/
export function licenseDeed(license: string): string | undefined {
if (license === 'CC0') return 'https://creativecommons.org/publicdomain/zero/1.0/';
const match = /^CC (BY(?:-SA)?) (\d\.\d)(?: ([a-z]{2}))?$/.exec(license);
if (!match) return undefined;
const [, terms, version, port] = match;
return `https://creativecommons.org/licenses/${terms.toLowerCase()}/${version}/${port ? `${port}/` : ''}`;
}
@@ -22,7 +22,7 @@ import {
} from 'lucide-react';
/** One thing a visitor can try in the demo right away: a tile of the welcome dialog. */
export interface DemoFeature {
interface DemoFeature {
id: string;
icon: LucideIcon;
titleKey: string;
@@ -0,0 +1,498 @@
[
{
"file": "(Venice) - Fondamente zattere - Casa Scarpa.jpg",
"author": "Didier Descouens",
"license": "CC BY-SA 4.0"
},
{
"file": "+Die Hallgrimskirkja ist das markanteste Bauwerk Reykjaviks. 03.jpg",
"author": "Holger Uwe Schmitt",
"license": "CC BY-SA 4.0"
},
{
"file": "+Die Hallgrimskirkja ist das markanteste Bauwerk Reykjaviks. 05.jpg",
"author": "Holger Uwe Schmitt",
"license": "CC BY-SA 4.0"
},
{
"file": "021 Wild smiling harbor seal at Jökulsárlón (Iceland) Photo by Giles Laurent.jpg",
"author": "Giles Laurent",
"license": "CC BY-SA 4.0"
},
{ "file": "03613 ISL Akureyri cruise vessel vapors V P.jpg", "author": "Virtual-Pano", "license": "CC BY-SA 4.0" },
{ "file": "20181110 Fushimi Inari shrine 3.jpg", "author": "Balon Greyjoy", "license": "CC0" },
{ "file": "20181110 Fushimi Inari shrine 4.jpg", "author": "Balon Greyjoy", "license": "CC0" },
{ "file": "20181110 Fushimi Inari shrine 6.jpg", "author": "Balon Greyjoy", "license": "CC0" },
{ "file": "2019-08-01 Zugspitze 7.jpg", "author": "Maksym Kozlenko", "license": "CC BY-SA 4.0" },
{
"file": "2022-05-28 13-21-39 Heinabergslón and Heinabergsjökull.jpg",
"author": "Hansueli Krapf",
"license": "CC BY-SA 4.0"
},
{ "file": "2022-05-28 13-26-26 Island - Hoffell 118.0.jpg", "author": "Hansueli Krapf", "license": "CC BY-SA 4.0" },
{
"file": "2022-05-28 13-50-35 Panorama of Heinabergsjökull.jpg",
"author": "Hansueli Krapf",
"license": "CC BY-SA 4.0"
},
{ "file": "2023-11-04 Lycian Way near Kaş.jpg", "author": "Alexkom000", "license": "CC BY 4.0" },
{ "file": "AbiskoNationalpark01.jpg", "author": "Unterrichter at English Wikipedia", "license": "Public domain" },
{ "file": "Almannagjá gorge at Þingvellir National Park.jpg", "author": "Olga Ernst", "license": "CC BY-SA 4.0" },
{ "file": "Almannagjá rift at Þingvellir National Park.jpg", "author": "Olga Ernst", "license": "CC BY-SA 4.0" },
{ "file": "Arashiyama Bamboo Grove (Unsplash).jpg", "author": "Erol Ahmed", "license": "CC0" },
{ "file": "Arashiyama Bamboo Grove.jpg", "author": "Mitchwandrew", "license": "CC BY 4.0" },
{ "file": "Arashiyama-Bamboo-Grove-Sunset.jpg", "author": "Bjørn Christian Tørrissen", "license": "CC BY-SA 4.0" },
{ "file": "ArashiyamaTogetsukyo Kyoto.jpg", "author": "Kentagon", "license": "CC BY-SA 4.0" },
{ "file": "Arnarstapi (port).jpg", "author": "Chmee2/Valtameri", "license": "CC BY 3.0" },
{ "file": "Arnarstapi 2.jpg", "author": "Christian Bickel", "license": "CC BY-SA 2.0 de" },
{ "file": "Arnarstapi 4.jpg", "author": "Christian Bickel", "license": "CC BY-SA 2.0 de" },
{
"file": "Averau from Fedare at Passo Giau, Belluno, Veneto, Italy, 2025 October.jpg",
"author": "Simo Räsänen",
"license": "CC BY-SA 4.0"
},
{ "file": "Bamboo Forest, Arashiyama, Kyoto, Japan.jpg", "author": "Basile Morin", "license": "CC BY-SA 4.0" },
{ "file": "Bamboo Grove, Arashiyama, Kyoto, Japan.jpg", "author": "Basile Morin", "license": "CC BY-SA 4.0" },
{ "file": "Basalt Arch near Arnarstapi.jpg", "author": "Remarksman", "license": "CC BY 2.0" },
{
"file": "Basilica di Santa Maria della Salute, Venice (37770336971).jpg",
"author": "Dimitris Kamaras from Athens, Greece",
"license": "CC BY 2.0"
},
{ "file": "Black Sand Beach Iceland, Reynisfjara Beach.jpg", "author": "miketnorton", "license": "CC BY 2.0" },
{ "file": "Bubbly Diamond on the Beach - Iceland.jpg", "author": "Eric Kilby", "license": "CC BY-SA 2.0" },
{
"file": "Burano Hausfassade Wäsche-20090315-RM-113017.jpg",
"author": "Reinhold Möller",
"license": "CC BY-SA 4.0"
},
{
"file": "Burano Hausfassade Wäsche-20090315-RM-113043.jpg",
"author": "Reinhold Möller",
"license": "CC BY-SA 4.0"
},
{ "file": "Burano Vendig Aug 2021 4.jpg", "author": "Kasa Fue", "license": "CC BY-SA 4.0" },
{ "file": "Café A Brasileira. Sculpture.jpg", "author": "MFREYNAUD51", "license": "CC BY-SA 4.0" },
{
"file": "Café de Flore, 172 boulevard Saint-Germain, Paris 6e.jpg",
"author": "Celette",
"license": "CC BY-SA 4.0"
},
{
"file": "Café Majestic - Porto - Portugal (53283088293).jpg",
"author": "Vitor Oliveira from Torres Vedras, PORTUGAL",
"license": "CC BY-SA 2.0"
},
{ "file": "Café Sperl, Wien.jpg", "author": "Jeremy Thompson", "license": "CC BY 2.0" },
{ "file": "Caffè San Marco-Trieste.jpg", "author": "Tiesse", "license": "Public domain" },
{ "file": "Caffè Sant'Eustachio.JPG", "author": "LPLT", "license": "CC BY-SA 3.0" },
{ "file": "Canale Grande in the morning light - panoramio.jpg", "author": "Schölla Schwarz", "license": "CC BY 3.0" },
{ "file": "Cinque Torri 01.jpg", "author": "Clemens Stockner", "license": "CC BY-SA 4.0" },
{ "file": "Cinque Torri 02.jpg", "author": "Clemens Stockner", "license": "CC BY-SA 4.0" },
{ "file": "Cinque Torri 1.jpg", "author": "kallerna", "license": "CC BY-SA 4.0" },
{ "file": "Cinque Torri, Dolomites - panoramio (1).jpg", "author": "Petr Kraumann", "license": "CC BY 3.0" },
{ "file": "Cinque Torri, Dolomites - panoramio (2).jpg", "author": "Petr Kraumann", "license": "CC BY 3.0" },
{
"file": "Colourful houses in Corte della Comare, Burano (detail).jpg",
"author": "Marco Ober",
"license": "CC BY-SA 4.0"
},
{ "file": "Croda del Becco and Rifugio Biella 202008.jpg", "author": "Marie-Lan Taÿ Pamart", "license": "CC BY 4.0" },
{
"file": "Cuernos del Paine in Torres del Paine National Park.jpg",
"author": "Pedro Szekely from Los Angeles, USA",
"license": "CC BY-SA 2.0"
},
{ "file": "Dettifoss 2026 02.jpg", "author": "Syrio", "license": "CC BY-SA 4.0" },
{ "file": "Dettifoss Panorama 01.jpg", "author": "Spike", "license": "CC BY-SA 4.0" },
{ "file": "Dettifoss Panorama 02.jpg", "author": "Spike", "license": "CC BY-SA 4.0" },
{ "file": "Dettifoss Panorama 03.jpg", "author": "Spike", "license": "CC BY-SA 4.0" },
{ "file": "Dorsoduro Rio dei Carmini in Venice.jpg", "author": "Wolfgang Moroder", "license": "CC BY-SA 3.0" },
{ "file": "Dotonbori Osaka Japan01-r.jpg", "author": "Oilstreet", "license": "CC BY 2.5" },
{ "file": "Dotonbori, Osaka, at night, November 2016.jpg", "author": "Martin Falbisoner", "license": "CC BY-SA 4.0" },
{ "file": "Drei Zinnen Tre Cime di Lavaredo Dolomites.jpg", "author": "Wolfgang Moroder", "license": "CC BY-SA 3.0" },
{ "file": "Dyrholaey Arch.jpg", "author": "Eric Kilby", "license": "CC BY-SA 2.0" },
{
"file": "Dyrhólaey, seen from Reynisfjara, Iceland, 20240720 0831 2824.jpg",
"author": "Jakub Hałun",
"license": "CC BY 4.0"
},
{ "file": "Eibsee und Ammergauer Alpen.jpg", "author": "Octagon", "license": "CC BY 3.0" },
{
"file": "Ëur de Bredles Secëda Odles y Sas Rigais da Resciesa.jpg",
"author": "Wolfgang Moroder",
"license": "CC BY-SA 4.0"
},
{ "file": "Faloria Cortina d'Ampezzo 10.jpg", "author": "kallerna", "license": "CC BY-SA 4.0" },
{ "file": "Falzarego-N.jpg", "author": "Supakon Mohr", "license": "CC BY-SA 3.0 de" },
{ "file": "Fimmvörðuháls (1).jpg", "author": "Chmee2/Valtameri", "license": "CC BY 3.0" },
{ "file": "Fimmvörðuháls (9).jpg", "author": "Chmee2/Valtameri", "license": "CC BY 3.0" },
{
"file": "Fimmvorduhals-emergency-hut.jpg",
"author": "Reen Eversdijk from Groningen, The Netherlands",
"license": "CC BY 2.0"
},
{ "file": "Fjaðrárgljúfur 04.jpg", "author": "Syrio", "license": "CC BY-SA 4.0" },
{ "file": "Forcella Nuvolau–Nuvolau lodge trail.jpg", "author": "Marco Ober", "license": "CC BY-SA 4.0" },
{
"file": "Four ladies wearing a yukata in front of the North Gate of Kiyomizu-dera temple Kyoto Japan.jpg",
"author": "Basile Morin",
"license": "CC BY-SA 4.0"
},
{
"file": "Frea pass Sella group Pisciadu Dolomites South Tyrol.jpg",
"author": "Wolfgang Moroder",
"license": "CC BY-SA 3.0"
},
{ "file": "Funivia del Lagazuoi (passo Falzarego).jpg", "author": "Ale041VE", "license": "CC BY-SA 4.0" },
{ "file": "Gion Hanamikôji-dôri.jpg", "author": "Yanajin33", "license": "CC BY-SA 4.0" },
{
"file": "Glacier du Mont Mallet (Glacier du Géant) & Mont Blanc, 2010 July.jpg",
"author": "Ximonic, Simo Räsänen",
"license": "CC BY-SA 3.0"
},
{ "file": "Godafoss 2.jpg", "author": "Tord Dellsen", "license": "CC0" },
{ "file": "Godafoss 3.jpg", "author": "Tord Dellsen", "license": "CC0" },
{ "file": "Godafoss 4.jpg", "author": "Tord Dellsen", "license": "CC0" },
{ "file": "Goðafoss Island.jpg", "author": "Superbass", "license": "CC BY-SA 3.0" },
{
"file": "Gondola Guido Lorenzi - Son Forca Cristallo Italy - panoramio.jpg",
"author": "Hiroki Ogawa",
"license": "CC BY 3.0"
},
{ "file": "GR20 Corsica Montains-2.jpg", "author": "Hermes from mars", "license": "CC BY-SA 3.0" },
{
"file": "Grand Canal Santa Maria della Salute Vaporetto Gondola Venice 01.jpg",
"author": "Julian Herzog",
"license": "CC BY 4.0"
},
{
"file": "Gullfoss, Suðurland, Islandia, 2014-08-16, DD 119.JPG",
"author": "Diego Delso",
"license": "CC BY-SA 4.0"
},
{ "file": "Hallgrímskirkja 1.jpg", "author": "Dmitry Brant", "license": "CC BY-SA 4.0" },
{ "file": "Heinabergsjökull (38322450122).jpg", "author": "Markus Trienke", "license": "CC BY-SA 2.0" },
{ "file": "Hverfjall July 2014.JPG", "author": "Martin Falbisoner", "license": "CC BY-SA 4.0" },
{ "file": "Hverir Námaskarð.jpg", "author": "JavierOlivares", "license": "CC BY-SA 4.0" },
{ "file": "Iceland Dettifoss 1972-4.jpg", "author": "Roger McLassus", "license": "CC BY-SA 3.0" },
{ "file": "Iceland Godafoss 1972.jpg", "author": "Roger McLassus", "license": "CC BY-SA 3.0" },
{ "file": "Il Sentiero degli dei.jpg", "author": "Stiflele", "license": "CC BY-SA 4.0" },
{
"file": "Illusion, Burano Island. (39893205060).jpg",
"author": "Mihai Lucîț from Baia Mare, Romania",
"license": "CC BY 2.0"
},
{ "file": "Islanda- Þórsmörk.jpg", "author": "Mario Falcetti", "license": "CC BY 4.0" },
{
"file": "Jökulsárlón glacier lagoon, Iceland, 20240719 0935 2448.jpg",
"author": "Jakub Hałun",
"license": "CC BY 4.0"
},
{
"file": "Jökulsárlón glacier lagoon, Iceland, 20240719 0936 2452.jpg",
"author": "Jakub Hałun",
"license": "CC BY 4.0"
},
{
"file": "Jökulsárlón glacier lagoon, Iceland, 20240719 0937 2454.jpg",
"author": "Jakub Hałun",
"license": "CC BY 4.0"
},
{
"file": "Jökulsárlón glacier lagoon, Iceland, 20240719 0947 2477.jpg",
"author": "Jakub Hałun",
"license": "CC BY 4.0"
},
{
"file": "Kansai Airport (2660200665).jpg",
"author": "Kentaro Iemoto from Tokyo, Japan",
"license": "CC BY-SA 2.0"
},
{ "file": "Kinkaku-ji in November 2016 -02.jpg", "author": "Martin Falbisoner", "license": "CC BY-SA 4.0" },
{
"file": "Kinkaku-ji the Golden Temple in Kyoto overlooking the lake - high rez.JPG",
"author": "Jaycangel",
"license": "CC BY-SA 3.0"
},
{ "file": "Kirkjufell in Fog (Unsplash).jpg", "author": "Nicholas Kampouris", "license": "CC0" },
{ "file": "Kirkjufell with its reflection.jpg", "author": "_大面", "license": "CC BY 2.5" },
{ "file": "Kirkjufell, Iceland (Unsplash LoKG7kKx7Zc).jpg", "author": "Steven Diaz", "license": "CC0" },
{
"file": "Kirkjufell, Vesturland, Islandia, 2014-08-14, DD 091.JPG",
"author": "Diego Delso",
"license": "CC BY-SA 4.0"
},
{ "file": "Kiyomizu-dera, Kyoto, November 2016 -01.jpg", "author": "Martin Falbisoner", "license": "CC BY-SA 4.0" },
{ "file": "Kiyomizu-dera, Kyoto, November 2016 -02.jpg", "author": "Martin Falbisoner", "license": "CC BY-SA 4.0" },
{ "file": "Kiyomizu-dera, Kyoto, November 2016 -03.jpg", "author": "Martin Falbisoner", "license": "CC BY-SA 4.0" },
{ "file": "Kiyomizu-dera, Kyoto, November 2016 -06.jpg", "author": "Martin Falbisoner", "license": "CC BY-SA 4.0" },
{ "file": "Kiyomizu-dera, Kyoto, November 2016 -07.jpg", "author": "Martin Falbisoner", "license": "CC BY-SA 4.0" },
{ "file": "KyotoFushimiInariLarge.jpg", "author": "Paul Vlaar", "license": "CC BY-SA 3.0" },
{ "file": "Lagazuoi 05.jpg", "author": "Clemens Stockner", "license": "CC BY-SA 4.0" },
{ "file": "Lago alla base di Svínafellsjökull 01.jpg", "author": "Syrio", "license": "CC BY-SA 4.0" },
{ "file": "Lago alla base di Svínafellsjökull 02.jpg", "author": "Syrio", "license": "CC BY-SA 4.0" },
{ "file": "Lago di Braies South Tyrol 3.jpg", "author": "kallerna", "license": "CC BY-SA 4.0" },
{ "file": "Lago di Braies South Tyrol 5.jpg", "author": "kallerna", "license": "CC BY-SA 4.0" },
{ "file": "Lago di Braies South Tyrol 6.jpg", "author": "kallerna", "license": "CC BY-SA 4.0" },
{ "file": "Lago di Braies South Tyrol 7.jpg", "author": "kallerna", "license": "CC BY-SA 4.0" },
{ "file": "Lago di Braies South Tyrol 9.jpg", "author": "kallerna", "license": "CC BY-SA 4.0" },
{
"file": "Lago di Limo, Parco Naturale Fanes - Sennes - Braies (BZ).jpg",
"author": "53cri021",
"license": "CC BY-SA 4.0"
},
{ "file": "Lagoa Rasa (Sete Cidades) 2.jpg", "author": "Walmaul", "license": "CC BY-SA 4.0" },
{ "file": "Lake Ashi & Mt Fuji & Hakone Shrine.jpg", "author": "Charlie fong", "license": "CC BY-SA 4.0" },
{ "file": "Landmannalaugar in summer 2009 (13).jpg", "author": "Chmee2/Valtameri", "license": "CC BY 3.0" },
{
"file": "Manarola NW Cemetery Corniglia Monterosso Cinque Terre Sep23 A7C 06872.jpg",
"author": "Timothy A. Gonsalves",
"license": "CC BY-SA 4.0"
},
{ "file": "Meiji Jingu Shrine Tokyo Japan.jpg", "author": "MediaByPanda", "license": "CC BY-SA 4.0" },
{ "file": "Meiji shrine in August 2007 5.jpg", "author": "Syced", "license": "CC0" },
{ "file": "Meiji Shrine, Tokyo; April 2009 (02).jpg", "author": "MIKI Yoshihito", "license": "CC BY 2.0" },
{
"file": "Meiji-jingū main hall vue centrale.jpg",
"author": "Hyppolyte de Saint-Rambert",
"license": "CC BY-SA 4.0"
},
{ "file": "Meiji-jingu-pathway.jpg", "author": "Nightcrafter", "license": "CC BY-SA 4.0" },
{ "file": "Monte Kirkjufell (Luglio 2024).jpg", "author": "Paolova97", "license": "CC BY-SA 4.0" },
{ "file": "Morning light in Venice (33726527456).jpg", "author": "Hervé Simon", "license": "CC BY-SA 2.0" },
{ "file": "Mount Fuji and Shinkansen N700.jpg", "author": "Alpsdake", "license": "CC BY-SA 4.0" },
{
"file": "Mount Myojingatake and Owakudani Station from Owakudani Valley.JPG",
"author": "そらみみ",
"license": "CC BY-SA 3.0"
},
{ "file": "Mount-Fuji from Tokaido Shinkansen window view.jpg", "author": "Indiana jo", "license": "CC BY-SA 4.0" },
{
"file": "Mountain and grotto at Reynisfjara Beach in July, 2019.jpg",
"author": "Alexander Grebenkov",
"license": "CC BY 3.0"
},
{ "file": "Múlafossur2.jpg", "author": "Ekrem Canli", "license": "CC BY-SA 4.0" },
{ "file": "Neon Signs in Dotonbori, Osaka 20190415 1.jpg", "author": "DXR", "license": "CC BY-SA 4.0" },
{
"file": "Neunerspitze, 2968 m und Zehnerspitze 3016 m.jpg",
"author": "2015 Michael 2015",
"license": "CC BY-SA 4.0"
},
{ "file": "NeunerZehnerFanes.JPG", "author": "Alex1011", "license": "CC BY 2.5" },
{ "file": "Nishiki ichiba kyoto.jpg", "author": "Pitan", "license": "CC BY-SA 3.0" },
{ "file": "Old Tokaido - Hakone.jpg", "author": "Quercus acuta", "license": "CC BY-SA 4.0" },
{ "file": "Old Tokaido -01.jpg", "author": "Aimaimyi", "license": "CC BY-SA 3.0" },
{ "file": "On the hillside above Ginkaku-ji (2099332601).jpg", "author": "KimonBerlin", "license": "CC BY-SA 2.0" },
{ "file": "Osaka Dotonbori Canal.jpg", "author": "32linesky", "license": "CC BY-SA 4.0" },
{
"file": "Owakudani Valley and Mount Kammurigatake near Owakudani Station 1.JPG",
"author": "そらみみ",
"license": "CC BY-SA 3.0"
},
{
"file": "Owakudani Valley and Mount Kammurigatake near Owakudani Station 2.JPG",
"author": "そらみみ",
"license": "CC BY-SA 3.0"
},
{ "file": "Owakudani Valley near Owakudani Station 2.JPG", "author": "そらみみ", "license": "CC BY-SA 3.0" },
{ "file": "Owakudani Valley near Owakudani Station.JPG", "author": "そらみみ", "license": "CC BY-SA 3.0" },
{ "file": "Palazzo Balbi (Venice).jpg", "author": "Didier Descouens", "license": "CC BY-SA 3.0" },
{
"file": "Palazzo dei Camerlenghi e Ponte Rialto a Venezia.jpg",
"author": "Wolfgang Moroder",
"license": "CC BY-SA 3.0"
},
{
"file": "Panorama of Canal Grande and Ponte di Rialto, Venice - September 2017.jpg",
"author": "Martin Falbisoner",
"license": "CC BY-SA 4.0"
},
{ "file": "Passo di Giau.jpg", "author": "Frisia Orientalis", "license": "CC BY-SA 3.0" },
{ "file": "Passo Giau SP638.jpg", "author": "kallerna", "license": "CC BY-SA 4.0" },
{ "file": "Photos at teamlab planets tokyo.jpg", "author": "Sasa0403", "license": "CC BY-SA 4.0" },
{ "file": "Ponte di Rialto facciata ovest di sera.jpg", "author": "Wolfgang Moroder", "license": "CC BY-SA 3.0" },
{ "file": "Ponte di Rialto Venice 1.jpg", "author": "kallerna", "license": "CC BY-SA 4.0" },
{ "file": "Pontile Zattere (Venezia).jpg", "author": "Didier Descouens", "license": "CC BY-SA 4.0" },
{ "file": "Preikestolen Norge.jpg", "author": "Stefan Krause, Germany", "license": "CC BY-SA 3.0" },
{ "file": "Ra Gusela, view from Passo Giau.jpg", "author": "Marco Ober", "license": "CC BY-SA 4.0" },
{ "file": "Reine i Lofoten LC0148.jpg", "author": "Jörg Hempel", "license": "CC BY-SA 2.0 de" },
{
"file": "Reynisfjara Black Sand Beach Iceland Travel Photography (210387065).jpeg",
"author": "Giuseppe Milo",
"license": "CC BY 3.0"
},
{
"file": "Reynisfjara, Suðurland, Islandia, 2014-08-17, DD 164.JPG",
"author": "Diego Delso",
"license": "CC BY-SA 4.0"
},
{
"file": "Reyniskirkja Church,Vík í Mýrdal, Suðurland,Ísland.jpg",
"author": "JavierOlivares",
"license": "CC BY-SA 4.0"
},
{ "file": "Rifugio Fanes 01.JPG", "author": "Bbruno", "license": "CC BY-SA 3.0" },
{ "file": "Rifugio Fanes.jpg", "author": "Llorenzi", "license": "CC BY-SA 3.0" },
{ "file": "Rigi Kulm cross 20210905.jpg", "author": "Daniel Kraft", "license": "CC BY-SA 3.0" },
{
"file": "Roca de la Ley, Parque Nacional de Þingvellir, Suðurland, Islandia, 2014-08-16, DD 022.JPG",
"author": "Diego Delso",
"license": "CC BY-SA 4.0"
},
{ "file": "Salzwelten vantage point.jpg", "author": "Grandmaster Huon", "license": "CC0" },
{ "file": "San Marco cathedral in Venice.JPG", "author": "Petar Milosevic", "license": "CC BY-SA 4.0" },
{
"file": "Santa Maria della Salute from Hotel Monaco nightview.jpg",
"author": "Wolfgang Moroder",
"license": "CC BY-SA 3.0"
},
{ "file": "Santa Maria della Salute from Hotel Monaco.jpg", "author": "Wolfgang Moroder", "license": "CC BY-SA 3.0" },
{ "file": "Seljalandsfoss - panoramio (7).jpg", "author": "Zoran Kurelić Rabko", "license": "CC BY-SA 3.0" },
{ "file": "Seljalandsfoss and Gljúfrabúi, July 2019.jpg", "author": "Alexander Grebenkov", "license": "CC BY 3.0" },
{ "file": "Seljalandsfoss July 2014 -1.JPG", "author": "Martin Falbisoner", "license": "CC BY-SA 4.0" },
{ "file": "Seljalandsfoss view from side, July 2019.jpg", "author": "Alexander Grebenkov", "license": "CC BY 3.0" },
{
"file": "Seljalandsfoss, Suðurland, Islandia, 2014-08-16, DD 189-191 HDR.JPG",
"author": "Diego Delso",
"license": "CC BY-SA 4.0"
},
{
"file": "Senso-ji Temple @ Asakusa (13824517393).jpg",
"author": "Guilhem Vellut from Annecy, France",
"license": "CC BY 2.0"
},
{
"file": "Senso-ji Temple @ Asakusa (13824525803).jpg",
"author": "Guilhem Vellut from Annecy, France",
"license": "CC BY 2.0"
},
{
"file": "Senso-ji Temple @ Asakusa (13824839284).jpg",
"author": "Guilhem Vellut from Annecy, France",
"license": "CC BY 2.0"
},
{
"file": "Senso-ji Temple @ Asakusa (14019477806).jpg",
"author": "Guilhem Vellut from Annecy, France",
"license": "CC BY 2.0"
},
{ "file": "Seyðisfjörður church 1.jpg", "author": "Saifunny", "license": "CC BY-SA 4.0" },
{ "file": "Seyðisfjörður Sept 2019 1.jpg", "author": "Kasa Fue", "license": "CC BY-SA 4.0" },
{
"file": "Seydisfjordur, East Iceland (51440969579).jpg",
"author": "Fabio Achilli from Milano, Italy",
"license": "CC BY 2.0"
},
{
"file": "Seydisfjordur, East Iceland (51441184230).jpg",
"author": "Fabio Achilli from Milano, Italy",
"license": "CC BY 2.0"
},
{ "file": "Seyðisfjörður, Iceland (28328269217).jpg", "author": "Joe deSousa", "license": "CC0" },
{
"file": "Shadows and windows at Hallgrimskirkja Reykjavik Iceland 2026-02-07 21-21-01 1.jpg",
"author": "G. Edward Johnson",
"license": "CC BY 4.0"
},
{
"file": "Shariden, Kinkaku-ji, Kyoto, East View 20130811 1.jpg",
"author": "DXR / Daniel Vorndran",
"license": "CC BY-SA 3.0"
},
{ "file": "Shinjuku-West Omoide-Yokocho.jpg", "author": "MaedaAkihiko", "license": "CC BY-SA 4.0" },
{
"file": "Shinkansen N700S series photographed at Higashi-Tagonoura with Mount Fuji in the background, 10 April 2023.jpg",
"author": "Takeshi Aida",
"license": "CC BY-SA 2.0"
},
{ "file": "Skogafoss-02-2018-gje.jpg", "author": "Gerd Eichmann", "license": "CC BY-SA 4.0" },
{ "file": "Sky Lagoon Cold Plunge.jpg", "author": "Laurenmcl", "license": "CC BY-SA 4.0" },
{ "file": "Soca River Valley, Slovenia.jpg", "author": "Barry", "license": "CC BY 2.0" },
{
"file": "Start of the Old Hakone Highway (9412055176).jpg",
"author": "Maarten Heerlien from Voorschoten, The Netherlands",
"license": "CC BY 2.0"
},
{
"file": "Strokkur, Área geotérmica de Geysir, Suðurland, Islandia, 2014-08-16, DD 086.JPG",
"author": "Diego Delso",
"license": "CC BY-SA 4.0"
},
{ "file": "Svínafellsjökull 01.jpg", "author": "Syrio", "license": "CC BY-SA 4.0" },
{ "file": "Svínafellsjökull glacier at sunset.jpg", "author": "Mikołaj Bańka", "license": "CC BY 4.0" },
{ "file": "Svínafellsjökull.JPG", "author": "Bromr at Dutch Wikipedia", "license": "CC BY-SA 3.0" },
{
"file": "Taroko-Gorge Hualien Taiwan Pudu-Bridge-at-Taroko-National-Park-01.jpg",
"author": "CEphoto, Uwe Aranas",
"license": "CC BY-SA 3.0"
},
{
"file": "The Lava Fields Of Þórsmörk Iceland (175491845).jpeg",
"author": "Ilya Grigorik",
"license": "CC BY-SA 3.0"
},
{ "file": "The park bench (2669222928).jpg", "author": "Rosino", "license": "CC BY-SA 2.0" },
{
"file": "The West Highland Way approaching the Heads of Glen Etive and Glen Coe - geograph.org.uk - 5014659.jpg",
"author": "Tim Heaton",
"license": "CC BY-SA 2.0"
},
{ "file": "Tokyo Shibuya Scramble Crossing 2018-10-09.jpg", "author": "Benh LIEU SONG", "license": "CC BY-SA 2.0" },
{
"file": "Torii on Lake Ashi @ Hakone Shrine (13776878594).jpg",
"author": "Guilhem Vellut from Annecy, France",
"license": "CC BY 2.0"
},
{
"file": "Torii on Lake Ashi @ Hakone Shrine (13776879364).jpg",
"author": "Guilhem Vellut from Annecy, France",
"license": "CC BY 2.0"
},
{
"file": "Torii path with lantern at Fushimi Inari Taisha Shrine, Kyoto, Japan.jpg",
"author": "Basile Morin",
"license": "CC BY-SA 4.0"
},
{ "file": "Trolltunga 2.jpg", "author": "Steinar Talmoen", "license": "CC BY-SA 3.0" },
{ "file": "Tsukiji Outer Market -04.jpg", "author": "Aimaimyi", "license": "CC BY-SA 3.0" },
{ "file": "Tsukiji Outer Market -05.jpg", "author": "Aimaimyi", "license": "CC BY-SA 3.0" },
{ "file": "Tsukiji Outer Market -06.jpg", "author": "Aimaimyi", "license": "CC BY-SA 3.0" },
{ "file": "Tsukiji Outer Market 2.jpg", "author": "Aw1805", "license": "CC BY-SA 3.0" },
{ "file": "Ushguli Svaneti Georgia.JPG", "author": "Florian Pinel", "license": "CC BY-SA 3.0" },
{ "file": "Vaporetto grand canal venice.jpg", "author": "John Salatas", "license": "CC BY-SA 4.0" },
{ "file": "Vaporetto, Grand Canal, Venice.jpg", "author": "Peter K Burian", "license": "CC BY-SA 4.0" },
{ "file": "Venice, Canal Grande, Dorsoduro, Plzo Balbi.jpg", "author": "Armin Kleiner", "license": "CC BY-SA 4.0" },
{ "file": "Venice, Italy. Morning light.jpg", "author": "Dupont66", "license": "CC BY 4.0" },
{ "file": "Via ferrata Ivano Dibona - panoramio (1).jpg", "author": "Pavel Špindler", "license": "CC BY 3.0" },
{ "file": "Via ferrata Ivano Dibona - panoramio (2).jpg", "author": "Pavel Špindler", "license": "CC BY 3.0" },
{ "file": "Via Ferrato Ivano Dibona 3.jpg", "author": "Schi11", "license": "CC BY-SA 4.0" },
{ "file": "View from Miradouro do Pico do Arieiro - Madeira 05.jpg", "author": "H. Zell", "license": "CC BY-SA 3.0" },
{ "file": "Vire via ferrata Ivano Dibona.jpg", "author": "Evelyne Chassagneux-Bonini", "license": "CC BY-SA 4.0" },
{
"file": "Water reflection of Kinkaku-ji Temple a sunny day, Kyoto, Japan.jpg",
"author": "Basile Morin",
"license": "CC BY-SA 4.0"
},
{
"file": "Water reflection of Kinkaku-ji Temple with blue sky and white clouds, Kyoto, Japan.jpg",
"author": "Basile Morin",
"license": "CC BY-SA 4.0"
},
{ "file": "Water transport in Venice at Rialto bridge.jpg", "author": "Wolfgang Moroder", "license": "CC BY-SA 3.0" },
{ "file": "Waterfalls of Iceland 23 02.jpg", "author": "Hornstrandir1", "license": "CC BY-SA 4.0" },
{ "file": "Wengental 2019-10-18.jpg", "author": "2015 Michael 2015", "license": "CC BY-SA 4.0" },
{ "file": "Wien, Café Central -- 2018 -- 3135.jpg", "author": "Dietmar Rabich", "license": "CC BY-SA 4.0" },
{ "file": "Yanaka Ginza 2.jpg", "author": "Christophe95", "license": "CC BY-SA 4.0" },
{
"file": "Yanaka Ginza shopping street (4041690725).jpg",
"author": "Yoshikazu TAKADA from Tokyo, Japan",
"license": "CC BY 2.0"
},
{ "file": "Yanaka ginza street.jpg", "author": "Kentin", "license": "CC BY-SA 3.0" },
{ "file": "Yanaka Ginza, Tokyo, Japan (6790100548).jpg", "author": "yeowatzup", "license": "CC BY 2.0" },
{ "file": "Þingvellir, Almannagjá 01.jpg", "author": "Syrio", "license": "CC BY-SA 4.0" },
{ "file": "Þingvellir, Almannagjá 07.jpg", "author": "Syrio", "license": "CC BY-SA 4.0" },
{ "file": "関西国際空港全体写真20220811.jpg", "author": "Ankou1192", "license": "CC BY-SA 4.0" }
]
+19 -3
View File
@@ -3,12 +3,20 @@ import { useAuthStore } from '../../store/authStore';
import { formatCountdown, secondsUntilReset } from './demoWelcomeModel';
/**
* The welcome dialog's state: whether it is still open, and the live countdown
* to the next hourly reset, ticking once a second in the server's time zone.
* Whether the photo credits have the dialog: not yet, now, or no longer
* (the visitor came back from them, and the focus returns to their link).
*/
type CreditsView = 'unseen' | 'open' | 'left';
/**
* The welcome dialog's state: whether it is still open, whether it shows the
* photo credits in place of the welcome, and the live countdown to the next
* hourly reset, ticking once a second in the server's time zone.
*/
export function useDemoWelcome() {
const timeZone = useAuthStore((s) => s.serverTimezone);
const [open, setOpen] = useState(true);
const [credits, setCredits] = useState<CreditsView>('unseen');
const [secondsLeft, setSecondsLeft] = useState(() => secondsUntilReset(new Date(), timeZone));
useEffect(() => {
@@ -17,5 +25,13 @@ export function useDemoWelcome() {
return () => clearInterval(timer);
}, [open, timeZone]);
return { open, close: () => setOpen(false), countdown: formatCountdown(secondsLeft) };
return {
open,
close: () => setOpen(false),
countdown: formatCountdown(secondsLeft),
creditsOpen: credits === 'open',
backFromCredits: credits === 'left',
openCredits: () => setCredits('open'),
closeCredits: () => setCredits('left'),
};
}