diff --git a/.vscode/settings.json b/.vscode/settings.json
index b1ff9f78..7f46718b 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -11,6 +11,7 @@
"exif",
"exifr",
"exiftool",
+ "favs",
"ghijklmnopqrstuv",
"hgetall",
"hset",
diff --git a/src/app/(static)/grid/page.tsx b/src/app/(static)/grid/page.tsx
index 04b49373..c9a4d5bc 100644
--- a/src/app/(static)/grid/page.tsx
+++ b/src/app/(static)/grid/page.tsx
@@ -1,10 +1,4 @@
-import {
- getPhotosCached,
- getPhotosCountCached,
- getUniqueCamerasCached,
- getUniqueFilmSimulationsCached,
- getUniqueTagsCached,
-} from '@/cache';
+import { getPhotosCached } from '@/cache';
import SiteGrid from '@/components/SiteGrid';
import { generateOgImageMetaForPhotos } from '@/photo';
import PhotoGrid from '@/photo/PhotoGrid';
@@ -17,7 +11,7 @@ import {
getPaginationForSearchParams,
} from '@/site/pagination';
import PhotoGridSidebar from '@/photo/PhotoGridSidebar';
-import { SHOW_FILM_SIMULATIONS } from '@/site/config';
+import { getPhotoSidebarDataCached } from '@/photo/data';
export const runtime = 'edge';
@@ -37,10 +31,7 @@ export default async function GridPage({ searchParams }: PaginationParams) {
simulations,
] = await Promise.all([
getPhotosCached({ limit }),
- getPhotosCountCached(),
- getUniqueTagsCached(),
- getUniqueCamerasCached(),
- SHOW_FILM_SIMULATIONS ? getUniqueFilmSimulationsCached() : [],
+ ...getPhotoSidebarDataCached(),
]);
const showMorePath = photosCount > photos.length
diff --git a/src/app/(static)/sets/page.tsx b/src/app/(static)/sets/page.tsx
index 9427e65d..dfcca476 100644
--- a/src/app/(static)/sets/page.tsx
+++ b/src/app/(static)/sets/page.tsx
@@ -1,17 +1,11 @@
-import {
- getPhotosCached,
- getPhotosCountCached,
- getUniqueCamerasCached,
- getUniqueFilmSimulationsCached,
- getUniqueTagsCached,
-} from '@/cache';
+import { getPhotosCached } from '@/cache';
import InfoBlock from '@/components/InfoBlock';
import RedirectOnDesktop from '@/components/RedirectOnDesktop';
import SiteGrid from '@/components/SiteGrid';
import { generateOgImageMetaForPhotos } from '@/photo';
import PhotoGridSidebar from '@/photo/PhotoGridSidebar';
+import { getPhotoSidebarDataCached } from '@/photo/data';
import { MAX_PHOTOS_TO_SHOW_OG } from '@/photo/image-response';
-import { SHOW_FILM_SIMULATIONS } from '@/site/config';
import { PATH_GRID } from '@/site/paths';
import { Metadata } from 'next';
@@ -26,12 +20,7 @@ export default async function SetsPage() {
tags,
cameras,
simulations,
- ] = await Promise.all([
- getPhotosCountCached(),
- getUniqueTagsCached(),
- getUniqueCamerasCached(),
- SHOW_FILM_SIMULATIONS ? getUniqueFilmSimulationsCached() : [],
- ]);
+ ] = await Promise.all(getPhotoSidebarDataCached());
return (
}
type={showAppleIcon && isCameraApple ? 'icon-first' : type}
badged={badged}
- dim={dim}
+ contrast={contrast}
hoverEntity={countOnHover}
/>
);
diff --git a/src/components/Badge.tsx b/src/components/Badge.tsx
index 541dfb89..c8078b01 100644
--- a/src/components/Badge.tsx
+++ b/src/components/Badge.tsx
@@ -2,30 +2,39 @@ import { clsx } from 'clsx';
export default function Badge({
children,
- type = 'primary',
+ type = 'large',
+ highContrast,
uppercase,
interactive,
}: {
children: React.ReactNode
- type?: 'primary' | 'secondary' | 'text-only'
+ type?: 'large' | 'small' | 'text-only'
+ highContrast?: boolean
uppercase?: boolean
interactive?: boolean
}) {
const stylesForType = () => {
switch (type) {
- case 'primary': return clsx(
- 'px-1.5 py-[0.3rem] rounded-md',
- 'bg-gray-100/80 dark:bg-gray-900/80',
- 'border border-gray-200/60 dark:border-gray-800/75'
- );
- case 'secondary': return clsx(
- 'px-[0.3rem] py-1 rounded-[0.25rem]',
- 'bg-gray-300/30 dark:bg-gray-700/50',
- 'text-medium',
- 'font-medium text-[0.7rem]',
- interactive && 'hover:text-gray-900 dark:hover:text-gray-100',
- interactive && 'active:bg-gray-200 dark:active:bg-gray-700/60',
- );
+ case 'large':
+ return clsx(
+ 'px-1.5 py-[0.3rem] rounded-md',
+ 'bg-gray-100/80 dark:bg-gray-900/80',
+ 'border border-gray-200/60 dark:border-gray-800/75'
+ );
+ case 'small':
+ return clsx(
+ 'px-[0.3rem] py-1 rounded-[0.25rem]',
+ 'text-[0.7rem] font-medium',
+ highContrast
+ ? 'text-invert bg-main'
+ : 'text-medium bg-gray-300/30 dark:bg-gray-700/50',
+ interactive && highContrast
+ ? 'hover:opacity-70'
+ : 'hover:text-gray-900 dark:hover:text-gray-100',
+ interactive && highContrast
+ ? 'active:opacity-90'
+ : 'active:bg-gray-200 dark:active:bg-gray-700/60',
+ );
}
};
return (
diff --git a/src/components/EntityLink.tsx b/src/components/EntityLink.tsx
index aad2dd48..220d20fa 100644
--- a/src/components/EntityLink.tsx
+++ b/src/components/EntityLink.tsx
@@ -6,7 +6,7 @@ import { clsx } from 'clsx';
export interface EntityLinkExternalProps {
type?: 'icon-last' | 'icon-first' | 'icon-only' | 'text-only'
badged?: boolean
- dim?: boolean
+ contrast?: 'low' | 'medium' | 'high'
}
export default function EntityLink({
@@ -17,8 +17,8 @@ export default function EntityLink({
title,
type = 'icon-first',
badged,
+ contrast,
hoverEntity,
- dim,
}: {
label: ReactNode
labelSmall?: ReactNode
@@ -44,13 +44,18 @@ export default function EntityLink({
className={clsx(
'inline-flex gap-[0.23rem]',
!badged && 'text-main hover:text-gray-900 dark:hover:text-gray-100',
- dim && 'text-dim',
+ contrast === 'low' && 'text-dim',
)}
>
{type !== 'icon-only' && <>
{badged
?
-
+
{renderLabel()}
@@ -61,9 +66,11 @@ export default function EntityLink({
{icon && type !== 'text-only' &&
{icon}
}
diff --git a/src/photo/PhotoGridSidebar.tsx b/src/photo/PhotoGridSidebar.tsx
index 0eec733c..999c3c25 100644
--- a/src/photo/PhotoGridSidebar.tsx
+++ b/src/photo/PhotoGridSidebar.tsx
@@ -5,12 +5,11 @@ import PhotoTag from '@/tag/PhotoTag';
import { FaTag } from 'react-icons/fa';
import { IoMdCamera } from 'react-icons/io';
import { PhotoDateRange, dateRangeForPhotos, photoQuantityText } from '.';
-import { Tags } from '@/tag';
-import PhotoFilmSimulation from
- '@/simulation/PhotoFilmSimulation';
-import PhotoFilmSimulationIcon from
- '@/simulation/PhotoFilmSimulationIcon';
+import { TAG_FAVS, Tags } from '@/tag';
+import PhotoFilmSimulation from '@/simulation/PhotoFilmSimulation';
+import PhotoFilmSimulationIcon from '@/simulation/PhotoFilmSimulationIcon';
import { FilmSimulations, sortFilmSimulationsWithCount } from '@/simulation';
+import FavsTag from '../tag/FavsTag';
export default function PhotoGridSidebar({
tags,
@@ -32,8 +31,14 @@ export default function PhotoGridSidebar({
{tags.length > 0 && }
- items={tags.map(({ tag, count }) =>
- tag === TAG_FAVS
+ ?
+ : [
+ getPhotosCountCached(),
+ getUniqueTagsCached().then(tags =>
+ ([tags.find(({ tag }) => tag === TAG_FAVS) ?? []] as Tags)
+ .concat(tags.filter(({ tag }) => tag !== TAG_FAVS))),
+ getUniqueCamerasCached(),
+ SHOW_FILM_SIMULATIONS ? getUniqueFilmSimulationsCached() : [],
+] as const;
diff --git a/src/simulation/PhotoFilmSimulation.tsx b/src/simulation/PhotoFilmSimulation.tsx
index 08bb8a02..7087be84 100644
--- a/src/simulation/PhotoFilmSimulation.tsx
+++ b/src/simulation/PhotoFilmSimulation.tsx
@@ -8,7 +8,7 @@ export default function PhotoFilmSimulation({
simulation,
type = 'icon-last',
badged = true,
- dim,
+ contrast,
countOnHover,
}: {
simulation: FilmSimulation
@@ -28,7 +28,7 @@ export default function PhotoFilmSimulation({
title={`Film Simulation: ${large}`}
type={type}
badged={badged}
- dim={dim}
+ contrast={contrast}
hoverEntity={countOnHover}
/>
);
diff --git a/src/site/globals.css b/src/site/globals.css
index 81c052e0..dc8ba265 100644
--- a/src/site/globals.css
+++ b/src/site/globals.css
@@ -132,4 +132,8 @@
@apply
text-red-500 dark:text-red-400
}
+ .bg-main {
+ @apply
+ bg-gray-900 dark:bg-gray-100
+ }
}
diff --git a/src/tag/FavsTag.tsx b/src/tag/FavsTag.tsx
new file mode 100644
index 00000000..fc92975a
--- /dev/null
+++ b/src/tag/FavsTag.tsx
@@ -0,0 +1,38 @@
+import { FaStar } from 'react-icons/fa';
+import EntityLink, { EntityLinkExternalProps } from '@/components/EntityLink';
+import { TAG_FAVS } from '.';
+import { pathForTag } from '@/site/paths';
+
+export default function FavsTag({
+ type,
+ badged,
+ contrast,
+ countOnHover,
+}: {
+ countOnHover?: number
+} & EntityLinkExternalProps) {
+ return (
+
+ {TAG_FAVS}
+
+
+ : TAG_FAVS}
+ href={pathForTag(TAG_FAVS)}
+ icon={!badged &&
+ }
+ type={type}
+ hoverEntity={countOnHover}
+ badged={badged}
+ contrast={contrast}
+ />
+ );
+}
diff --git a/src/tag/PhotoTag.tsx b/src/tag/PhotoTag.tsx
index 800833fc..ecc4efaf 100644
--- a/src/tag/PhotoTag.tsx
+++ b/src/tag/PhotoTag.tsx
@@ -7,7 +7,7 @@ export default function PhotoTag({
tag,
type,
badged,
- dim,
+ contrast,
countOnHover,
}: {
tag: string
@@ -23,7 +23,7 @@ export default function PhotoTag({
/>}
type={type}
badged={badged}
- dim={dim}
+ contrast={contrast}
hoverEntity={countOnHover}
/>
);
diff --git a/src/tag/TagHeader.tsx b/src/tag/TagHeader.tsx
index 7d63ed21..f320de7d 100644
--- a/src/tag/TagHeader.tsx
+++ b/src/tag/TagHeader.tsx
@@ -1,8 +1,9 @@
import { Photo, PhotoDateRange } from '@/photo';
import PhotoTag from './PhotoTag';
-import { descriptionForTaggedPhotos } from '.';
+import { descriptionForTaggedPhotos, isTagFavs } from '.';
import { pathForTagShare } from '@/site/paths';
import PhotoSetHeader from '@/photo/PhotoSetHeader';
+import FavsTag from './FavsTag';
export default function TagHeader({
tag,
@@ -19,7 +20,9 @@ export default function TagHeader({
}) {
return (
}
+ entity={isTagFavs(tag)
+ ?
+ : }
entityVerb="Tagged"
entityDescription={descriptionForTaggedPhotos(photos, undefined, count)}
photos={photos}
diff --git a/src/tag/index.ts b/src/tag/index.ts
index e3ab8bb7..1b773b93 100644
--- a/src/tag/index.ts
+++ b/src/tag/index.ts
@@ -7,6 +7,8 @@ import {
import { absolutePathForTag, absolutePathForTagImage } from '@/site/paths';
import { capitalizeWords } from '@/utility/string';
+export const TAG_FAVS = 'favs';
+
export type Tags = {
tag: string
count: number
@@ -50,3 +52,5 @@ export const generateMetaForTag = (
descriptionForTaggedPhotos(photos, true, explicitCount, explicitDateRange),
images: absolutePathForTagImage(tag),
});
+
+export const isTagFavs = (tag: string) => tag === TAG_FAVS;