Refactor paths/escape handling, add test coverage

This commit is contained in:
Sam Becker 2023-09-29 13:40:08 -05:00
parent f126f4b0a7
commit 3d279cdab5
22 changed files with 2106 additions and 83 deletions

View File

@ -14,6 +14,7 @@
"thephotoblog",
"trpc",
"unnest",
"UsKSGcbt",
"WRHGZC",
"zadd",
"zrange"

81
__tests__/path.test.ts Normal file
View File

@ -0,0 +1,81 @@
import '@testing-library/jest-dom';
import {
getEscapePath,
getPathComponents,
isPathPhoto,
isPathPhotoShare,
isPathTag,
isPathTagPhoto,
isPathTagPhotoShare,
isPathTagShare,
} from '@/site/paths';
const PHOTO_ID = 'UsKSGcbt';
const TAG = 'tag-name';
const SHARE = 'share';
const PATH_ROOT = '/';
const PATH_GRID = '/grid';
const PATH_ADMIN = '/admin/photos';
const PATH_PHOTO = `/p/${PHOTO_ID}`;
const PATH_PHOTO_SHARE = `${PATH_PHOTO}/${SHARE}`;
const PATH_TAG = `/t/${TAG}`;
const PATH_TAG_SHARE = `${PATH_TAG}/${SHARE}`;
const PATH_TAG_PHOTO = `${PATH_TAG}/${PHOTO_ID}`;
const PATH_TAG_PHOTO_SHARE = `${PATH_TAG}/${PHOTO_ID}/${SHARE}`;
describe('Paths', () => {
it('can be classified', () => {
// Positive
expect(isPathPhoto(PATH_PHOTO)).toBe(true);
expect(isPathPhotoShare(PATH_PHOTO_SHARE)).toBe(true);
expect(isPathTag(PATH_TAG)).toBe(true);
expect(isPathTagShare(PATH_TAG_SHARE)).toBe(true);
expect(isPathTagPhoto(PATH_TAG_PHOTO)).toBe(true);
expect(isPathTagPhotoShare(PATH_TAG_PHOTO_SHARE)).toBe(true);
// Negative
expect(isPathPhoto(PATH_TAG_PHOTO_SHARE)).toBe(false);
expect(isPathPhotoShare(PATH_TAG_PHOTO)).toBe(false);
expect(isPathTag(PATH_TAG_SHARE)).toBe(false);
expect(isPathTagShare(PATH_TAG)).toBe(false);
expect(isPathTagPhoto(PATH_PHOTO_SHARE)).toBe(false);
expect(isPathTagPhotoShare(PATH_PHOTO)).toBe(false);
});
it('can be parsed', () => {
expect(getPathComponents(PATH_ROOT)).toEqual({});
expect(getPathComponents(PATH_PHOTO)).toEqual({
photoId: PHOTO_ID,
});
expect(getPathComponents(PATH_PHOTO_SHARE)).toEqual({
photoId: PHOTO_ID,
});
expect(getPathComponents(PATH_TAG)).toEqual({
tag: TAG,
});
expect(getPathComponents(PATH_TAG_SHARE)).toEqual({
tag: TAG,
});
expect(getPathComponents(PATH_TAG_PHOTO)).toEqual({
photoId: PHOTO_ID,
tag: TAG,
});
expect(getPathComponents(PATH_TAG_PHOTO_SHARE)).toEqual({
photoId: PHOTO_ID,
tag: TAG,
});
});
it('can be escaped', () => {
// Root views
expect(getEscapePath(PATH_ROOT)).toEqual(undefined);
expect(getEscapePath(PATH_GRID)).toEqual(undefined);
expect(getEscapePath(PATH_ADMIN)).toEqual(undefined);
// Photo views
expect(getEscapePath(PATH_PHOTO)).toEqual(PATH_GRID);
expect(getEscapePath(PATH_PHOTO_SHARE)).toEqual(PATH_PHOTO);
// Tag views
expect(getEscapePath(PATH_TAG)).toEqual(PATH_GRID);
expect(getEscapePath(PATH_TAG_SHARE)).toEqual(PATH_TAG);
expect(getEscapePath(PATH_TAG_PHOTO)).toEqual(PATH_TAG);
expect(getEscapePath(PATH_TAG_PHOTO_SHARE)).toEqual(PATH_TAG_PHOTO);
});
});

19
jest.config.mjs Normal file
View File

@ -0,0 +1,19 @@
/* eslint-disable max-len */
import nextJest from 'next/jest.js';
const createJestConfig = nextJest({
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment
dir: './',
});
// Add any custom config to be passed to Jest
/** @type {import('jest').Config} */
const config = {
// Add more setup options before each test is run
// setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
testEnvironment: 'jest-environment-jsdom',
};
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
export default createJestConfig(config);

View File

@ -5,11 +5,15 @@
"build": "next build",
"start": "next start",
"lint": "next lint",
"test": "jest --watch",
"analyze": "ANALYZE=true next build"
},
"dependencies": {
"@next/bundle-analyzer": "^13.5.3",
"@tailwindcss/forms": "^0.5.6",
"@testing-library/jest-dom": "^6.1.3",
"@testing-library/react": "^14.0.0",
"@types/jest": "^29.5.5",
"@types/node": "^20.7.1",
"@types/react": "18.2.23",
"@types/react-dom": "18.2.8",
@ -24,6 +28,8 @@
"eslint": "8.50.0",
"eslint-config-next": "13.5.3",
"framer-motion": "^10.16.4",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"nanoid": "^5.0.1",
"next": "^13.5.3",
"next-auth": "0.0.0-manual.5749b095",

1796
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -13,7 +13,11 @@ import {
import { FaRegEdit } from 'react-icons/fa';
import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus';
import { pathForBlobUrl } from '@/services/blob';
import { pathForPhoto, pathForPhotoEdit } from '@/site/paths';
import {
pathForAdminPhotos,
pathForPhoto,
pathForPhotoEdit,
} from '@/site/paths';
import { getPhotosLimitForQuery, titleForPhoto } from '@/photo';
import MorePhotos from '@/components/MorePhotos';
import {
@ -123,7 +127,7 @@ export default async function AdminPage({
</Fragment>)}
</AdminGrid>
{showMorePhotos &&
<MorePhotos path={`/admin/photos?next=${offset + 1}`} />}
<MorePhotos path={pathForAdminPhotos(offset + 1)} />}
</div>
</div>
</div>}

View File

@ -10,6 +10,7 @@ import { generateOgImageMetaForPhotos, getPhotosLimitForQuery } from '@/photo';
import PhotoGrid from '@/photo/PhotoGrid';
import PhotosEmptyState from '@/photo/PhotosEmptyState';
import { MAX_PHOTOS_TO_SHOW_HOME } from '@/photo/image-response';
import { pathForGrid } from '@/site/paths';
import PhotoTag from '@/tag/PhotoTag';
import { Metadata } from 'next';
@ -45,7 +46,7 @@ export default async function GridPage({
contentMain={<div className="space-y-4">
<PhotoGrid photos={photos} />
{showMorePhotos &&
<MorePhotos path={`/grid?next=${offset + 1}`} />}
<MorePhotos path={pathForGrid(offset + 1)} />}
</div>}
contentSide={tags &&
<AnimateItems

View File

@ -2,6 +2,7 @@ import { getPhotosCached, getPhotosCountCached } from '@/cache';
import MorePhotos from '@/components/MorePhotos';
import { getPhotosLimitForQuery } from '@/photo';
import StaggeredOgPhotos from '@/photo/StaggeredOgPhotos';
import { pathForOg } from '@/site/paths';
export const runtime = 'edge';
@ -28,7 +29,7 @@ export default async function GridPage({
<StaggeredOgPhotos photos={photos} />
</div>
{showMorePhotos &&
<MorePhotos path={`/og?next=${offset + 1}`} />}
<MorePhotos path={pathForOg(offset + 1)} />}
</div>
);
}

View File

@ -5,7 +5,11 @@ import {
} from '@/photo';
import { Metadata } from 'next';
import { redirect } from 'next/navigation';
import { absolutePathForPhoto, absolutePathForPhotoImage } from '@/site/paths';
import {
PATH_ROOT,
absolutePathForPhoto,
absolutePathForPhotoImage,
} from '@/site/paths';
import PhotoDetailPage from '@/photo/PhotoDetailPage';
import { getPhotoCached, getPhotosCached } from '@/cache';
import { getPhotos } from '@/services/postgres';
@ -58,7 +62,7 @@ export default async function PhotoPage({
}) {
const photo = await getPhotoCached(photoId);
if (!photo) { redirect('/'); }
if (!photo) { redirect(PATH_ROOT); }
const [
photosBefore,

View File

@ -1,5 +1,6 @@
import { getPhotoCached } from '@/cache';
import PhotoShareModal from '@/photo/PhotoShareModal';
import { PATH_ROOT } from '@/site/paths';
import { redirect } from 'next/navigation';
export const runtime = 'edge';
@ -11,7 +12,7 @@ export default async function Share({
}) {
const photo = await getPhotoCached(photoId);
if (!photo) { return redirect('/'); }
if (!photo) { return redirect(PATH_ROOT); }
return <PhotoShareModal photo={photo} />;
}

View File

@ -5,6 +5,7 @@ import SiteGrid from '@/components/SiteGrid';
import { generateOgImageMetaForPhotos, getPhotosLimitForQuery } from '@/photo';
import PhotoLarge from '@/photo/PhotoLarge';
import PhotosEmptyState from '@/photo/PhotosEmptyState';
import { pathForRoot } from '@/site/paths';
import { Metadata } from 'next';
export const runtime = 'edge';
@ -49,7 +50,7 @@ export default async function HomePage({
/>
{showMorePhotos &&
<SiteGrid
contentMain={<MorePhotos path={`?next=${offset + 1}`} />}
contentMain={<MorePhotos path={pathForRoot(offset + 1)} />}
/>}
</div>
: <PhotosEmptyState />

View File

@ -4,7 +4,11 @@ import {
} from '@/photo';
import { Metadata } from 'next';
import { redirect } from 'next/navigation';
import { absolutePathForPhoto, absolutePathForPhotoImage } from '@/site/paths';
import {
PATH_ROOT,
absolutePathForPhoto,
absolutePathForPhotoImage,
} from '@/site/paths';
import PhotoDetailPage from '@/photo/PhotoDetailPage';
import { getPhotoCached, getPhotosCached } from '@/cache';
import { getPhotos, getUniqueTags } from '@/services/postgres';
@ -64,7 +68,7 @@ export default async function PhotoTagPage({
}) {
const photo = await getPhotoCached(photoId);
if (!photo) { redirect('/'); }
if (!photo) { redirect(PATH_ROOT); }
const photos = await getPhotosCached({ tag });

View File

@ -1,6 +1,7 @@
import { getPhotoCached } from '@/cache';
import PhotoShareModal from '@/photo/PhotoShareModal';
import { getPhotos, getUniqueTags } from '@/services/postgres';
import { PATH_ROOT } from '@/site/paths';
import { redirect } from 'next/navigation';
export async function generateStaticParams() {
@ -24,7 +25,7 @@ export default async function Share({
}) {
const photo = await getPhotoCached(photoId);
if (!photo) { return redirect('/'); }
if (!photo) { return redirect(PATH_ROOT); }
return <PhotoShareModal photo={photo} tag={tag} />;
}

View File

@ -7,6 +7,7 @@ import StateProvider from '@/state/AppStateProvider';
import ThemeProviderClient from '@/site/ThemeProviderClient';
import Nav from '@/components/Nav';
import ToasterWithThemes from '@/components/ToasterWithThemes';
import PhotoEscapeHandler from '@/photo/PhotoEscapeHandler';
import '../site/globals.css';
@ -77,6 +78,7 @@ export default function RootLayout({
</StateProvider>
<Analytics />
</main>
<PhotoEscapeHandler />
<ToasterWithThemes />
</ThemeProviderClient>
</body>

View File

@ -1,5 +1,6 @@
import { auth } from '@/auth';
import SignInForm from '@/auth/SignInForm';
import { PATH_ROOT } from '@/site/paths';
import { cc } from '@/utility/css';
import { redirect } from 'next/navigation';
@ -9,7 +10,7 @@ export default async function SignInPage() {
const session = await auth();
if (session?.user) {
redirect('/');
redirect(PATH_ROOT);
}
return (

View File

@ -6,6 +6,7 @@ import { cc } from '@/utility/css';
import useClickInsideOutside from '@/utility/useClickInsideOutside';
import { useRouter } from 'next/navigation';
import AnimateItems from './AnimateItems';
import { PATH_ROOT } from '@/site/paths';
export default function Modal({
onClosePath,
@ -28,7 +29,10 @@ export default function Modal({
useClickInsideOutside({
htmlElements,
onClickOutside: () => router.push(onClosePath ?? '/', { scroll: false}),
onClickOutside: () => router.push(
onClosePath ?? PATH_ROOT,
{ scroll: false },
),
});
return (

View File

@ -6,6 +6,12 @@ import Link from 'next/link';
import SiteGrid from './SiteGrid';
import { SITE_DOMAIN_OR_TITLE } from '@/site/config';
import ViewSwitcher, { SwitcherSelection } from '@/photo/ViewSwitcher';
import {
PATH_ADMIN,
PATH_ROOT,
isPathGrid,
isPathProtected,
} from '@/site/paths';
export default function Nav({ showTextLinks }: { showTextLinks?: boolean }) {
const isLoggedIn = false;
@ -23,11 +29,11 @@ export default function Nav({ showTextLinks }: { showTextLinks?: boolean }) {
: <button onClick={linkOrAction}>{text}</button>;
const switcherSelectionForPath = (): SwitcherSelection | undefined => {
if (pathname === '/') {
if (pathname === PATH_ROOT) {
return 'full-frame';
} else if (pathname === '/grid') {
} else if (isPathGrid(pathname)) {
return 'grid';
} else if (pathname.startsWith('/admin')) {
} else if (isPathProtected(pathname)) {
return 'admin';
}
};
@ -47,12 +53,12 @@ export default function Nav({ showTextLinks }: { showTextLinks?: boolean }) {
showAdmin={isLoggedIn}
/>
{showTextLinks && <>
{renderLink('Home', '/')}
{renderLink('Admin', '/admin')}
{renderLink('Home', PATH_ROOT)}
{renderLink('Admin', PATH_ADMIN)}
</>}
</div>
<div className="hidden xs:block">
{renderLink(SITE_DOMAIN_OR_TITLE, '/')}
{renderLink(SITE_DOMAIN_OR_TITLE, PATH_ROOT)}
</div>
</>}
</div>

View File

@ -0,0 +1,27 @@
'use client';
import { getEscapePath } from '@/site/paths';
import { useRouter, usePathname } from 'next/navigation';
import { useEffect } from 'react';
const LISTENER_KEYUP = 'keyup';
export default function PhotoEscapeHandler() {
const router = useRouter();
const pathname = usePathname();
const escapePath = getEscapePath(pathname);
useEffect(() => {
const onKeyUp = (e: KeyboardEvent) => {
if (e.key.toUpperCase() === 'ESCAPE' && escapePath) {
router.push(escapePath, { scroll: false });
};
};
window.addEventListener(LISTENER_KEYUP, onKeyUp);
return () => window.removeEventListener(LISTENER_KEYUP, onKeyUp);
}, [router, escapePath]);
return null;
}

View File

@ -2,61 +2,44 @@ import { Photo } from '.';
import PhotoSmall from './PhotoSmall';
import { cc } from '@/utility/css';
import AnimateItems from '@/components/AnimateItems';
import Link from 'next/link';
const PHOTOS_PER_PAGE = 6;
const PHOTOS_MAX = 35;
export default function PhotoGrid({
photos,
selectedPhoto,
tag,
offset = 0,
fast,
animate = true,
animateOnFirstLoadOnly,
staggerOnFirstLoadOnly = true,
showMore,
}: {
photos: Photo[]
selectedPhoto?: Photo
tag?: string
offset?: number
fast?: boolean
animate?: boolean
animateOnFirstLoadOnly?: boolean
staggerOnFirstLoadOnly?: boolean
showMore?: boolean
}) {
return (
<>
<AnimateItems
className={cc(
'grid gap-1',
'grid-cols-2 sm:grid-cols-4 md:grid-cols-3 lg:grid-cols-4',
'items-center',
)}
type={animate === false ? 'none' : undefined}
duration={fast ? 0.3 : undefined}
staggerDelay={0.075}
distanceOffset={40}
animateOnFirstLoadOnly={animateOnFirstLoadOnly}
staggerOnFirstLoadOnly={staggerOnFirstLoadOnly}
items={photos.map(photo =>
<PhotoSmall
key={photo.id}
photo={photo}
tag={tag}
selected={photo.id === selectedPhoto?.id}
/>)}
/>
{showMore && (offset + PHOTOS_PER_PAGE) < PHOTOS_MAX &&
<Link
className="button mt-12"
href={`/grid/${offset + PHOTOS_PER_PAGE}`}
>
More
</Link>}
</>
<AnimateItems
className={cc(
'grid gap-1',
'grid-cols-2 sm:grid-cols-4 md:grid-cols-3 lg:grid-cols-4',
'items-center',
)}
type={animate === false ? 'none' : undefined}
duration={fast ? 0.3 : undefined}
staggerDelay={0.075}
distanceOffset={40}
animateOnFirstLoadOnly={animateOnFirstLoadOnly}
staggerOnFirstLoadOnly={staggerOnFirstLoadOnly}
items={photos.map(photo =>
<PhotoSmall
key={photo.id}
photo={photo}
tag={tag}
selected={photo.id === selectedPhoto?.id}
/>)}
/>
);
};

View File

@ -8,6 +8,8 @@ import { pathForPhoto } from '@/site/paths';
import { useAppState } from '@/state';
import { AnimationConfig } from '@/components/AnimateItems';
const LISTENER_KEYUP = 'keyup';
const ANIMATION_LEFT: AnimationConfig = { type: 'left', duration: 0.3 };
const ANIMATION_RIGHT: AnimationConfig = { type: 'right', duration: 0.3 };
@ -44,14 +46,17 @@ export default function PhotoLinks({
router.push(pathForPhoto(nextPhoto, tag), { scroll: false });
}
break;
case 'ESCAPE':
router.push('/grid');
break;
};
};
window.addEventListener('keyup', onKeyUp);
return () => window.removeEventListener('keyup', onKeyUp);
}, [router, setNextPhotoAnimation, previousPhoto, nextPhoto, tag]);
window.addEventListener(LISTENER_KEYUP, onKeyUp);
return () => window.removeEventListener(LISTENER_KEYUP, onKeyUp);
}, [
router,
setNextPhotoAnimation,
previousPhoto,
nextPhoto,
tag,
]);
return (
<>

View File

@ -2,6 +2,7 @@ import Switcher from '@/components/Switcher';
import SwitcherItem from '@/components/SwitcherItem';
import IconFullFrame from '@/icons/IconFullFrame';
import IconGrid from '@/icons/IconGrid';
import { PATH_GRID } from '@/site/paths';
import { BiLockAlt } from 'react-icons/bi';
export type SwitcherSelection = 'full-frame' | 'grid' | 'admin';
@ -23,7 +24,7 @@ export default function ViewSwitcher({
/>
<SwitcherItem
icon={<IconGrid />}
href="/grid"
href={PATH_GRID}
active={currentSelection === 'grid'}
noPadding
/>

View File

@ -1,55 +1,143 @@
import { Photo } from '@/photo';
import { BASE_URL } from './config';
// Prefixes
const PREFIX_PHOTO = '/p';
const PREFIX_TAG = '/t';
const PREFIX_ADMIN = '/admin';
// Modifiers
const SHARE = 'share';
const NEXT = 'next';
export const PATH_ADMIN_PHOTOS = `${PREFIX_ADMIN}/photos`;
export const PATH_ADMIN_UPLOAD = `${PREFIX_ADMIN}/uploads`;
// Core paths
export const PATH_ROOT = '/';
export const PATH_GRID = '/grid';
export const PATH_ADMIN = '/admin';
export const PATH_OG = '/og';
// Extended paths
export const PATH_ADMIN_PHOTOS = `${PATH_ADMIN}/photos`;
export const PATH_ADMIN_UPLOAD = `${PATH_ADMIN}/uploads`;
export const PATH_ADMIN_UPLOAD_BLOB_HANDLER = `${PATH_ADMIN_UPLOAD}/blob`;
// Absolute paths
export const ABSOLUTE_PATH_FOR_HOME_IMAGE = `${BASE_URL}/home-image`;
export const pathForPhoto = (photo: Photo, tag?: string) =>
tag
? `${pathForTag(tag)}/${photo.id}`
: `${PREFIX_PHOTO}/${photo.id}`;
const pathWithNext = (path: string, next?: number) =>
next !== undefined ? `${path}?${NEXT}=${next}` : path;
export const pathForPhotoShare = (photo: Photo, tag?: string) =>
export const pathForRoot = (next?: number) =>
pathWithNext(PATH_ROOT, next);
export const pathForGrid = (next?: number) =>
pathWithNext(PATH_GRID, next);
export const pathForAdminPhotos = (next?: number) =>
pathWithNext(PATH_ADMIN_PHOTOS, next);
export const pathForOg = (next?: number) =>
pathWithNext(PATH_OG, next);
type PhotoOrPhotoId = Photo | string;
const getPhotoId = (photoOrPhotoId: PhotoOrPhotoId) =>
typeof photoOrPhotoId === 'string' ? photoOrPhotoId : photoOrPhotoId.id;
export const pathForPhoto = (photo: PhotoOrPhotoId, tag?: string) =>
tag
? `${pathForTag(tag)}/${getPhotoId(photo)}`
: `${PREFIX_PHOTO}/${getPhotoId(photo)}`;
export const pathForPhotoShare = (photo: PhotoOrPhotoId, tag?: string) =>
`${pathForPhoto(photo, tag)}/${SHARE}`;
export const pathForPhotoEdit = (photo: Photo) =>
`${PATH_ADMIN_PHOTOS}/${photo.id}/edit`;
export const pathForPhotoEdit = (photo: PhotoOrPhotoId) =>
`${PATH_ADMIN_PHOTOS}/${getPhotoId(photo)}/edit`;
export const pathForTag = (tag: string) => `${PREFIX_TAG}/${tag}`;
export const pathForTagShare = (tag: string) =>
`${pathForTag(tag)}/${SHARE}`;
export const absolutePathForPhoto = (photo: Photo, tag?: string) =>
export const absolutePathForPhoto = (photo: PhotoOrPhotoId, tag?: string) =>
`${BASE_URL}${pathForPhoto(photo, tag)}`;
export const absolutePathForTag = (tag: string) =>
`${BASE_URL}${pathForTag(tag)}`;
export const absolutePathForPhotoImage = (photo: Photo) =>
export const absolutePathForPhotoImage = (photo: PhotoOrPhotoId) =>
`${absolutePathForPhoto(photo)}/image`;
export const absolutePathForTagImage = (tag: string) =>
`${absolutePathForTag(tag)}/image`;
// p/[photoId]
export const isPathPhoto = (pathname = '') =>
/^\/p\/[^/]+\/?$/.test(pathname);
// p/[photoId]/share
export const isPathPhotoShare = (pathname = '') =>
/^\/p\/[^/]+\/share\/?$/.test(pathname);
// t/[tagId]
export const isPathTag = (pathname = '') =>
/^\/t\/[^/]+\/?$/.test(pathname);
// t/[tagId]/share
export const isPathTagShare = (pathname = '') =>
/^\/t\/[^/]+\/share\/?$/.test(pathname);
// t/[tagId]/[photoId]
export const isPathTagPhoto = (pathname = '') =>
/^\/t\/[^/]+\/[^/]+\/?$/.test(pathname);
// t/[tagId]/[photoId]/share
export const isPathTagPhotoShare = (pathname = '') =>
/^\/t\/[^/]+\/[^/]+\/share\/?$/.test(pathname);
export const isPathGrid = (pathname = '') =>
pathname.startsWith(PATH_GRID);
export const isPathSignIn = (pathname = '') =>
pathname.startsWith('/sign-in');
export const isPathAdmin = (pathname = '') =>
pathname.startsWith('/admin');
export const isPathProtected = (pathname = '') =>
pathname.startsWith(PREFIX_ADMIN) ||
pathname.startsWith(PATH_ADMIN) ||
pathname === '/checklist';
export const getPathComponents = (pathname = ''): {
photoId?: string
tag?: string
} => {
const photoIdFromPhoto = pathname.match(/^\/p\/([^/]+)/)?.[1];
const photoIdFromTag = pathname.match(/^\/t\/[^/]+\/((?!share)[^/]+)/)?.[1];
const tag = pathname.match(/^\/t\/([^/]+)/)?.[1];
return {
photoId: (
photoIdFromPhoto ||
photoIdFromTag
),
tag,
};
};
export const getEscapePath = (pathname?: string) => {
const { photoId, tag } = getPathComponents(pathname);
if (
(photoId && isPathPhoto(pathname)) ||
(tag && isPathTag(pathname))
) {
return PATH_GRID;
} else if (photoId && isPathTagPhotoShare(pathname)) {
return pathForPhoto(photoId, tag);
} else if (photoId && isPathPhotoShare(pathname)) {
return pathForPhoto(photoId);
} else if (tag && (isPathTagPhoto(pathname) || isPathTagShare(pathname))) {
return pathForTag(tag);
} else if (tag && isPathTagShare(pathname)) {
return pathForTag(tag);
}
};