From 74e91be0018cc44ffdf7a9bc1fad2332e4aedf25 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Sat, 1 Mar 2025 13:00:43 -0600 Subject: [PATCH] Offer sidebar ordering with paired insight --- README.md | 2 +- src/admin/AdminAppConfigurationClient.tsx | 10 + src/admin/AdminAppInfoIcon.tsx | 4 +- src/admin/AdminInfoNav.tsx | 4 +- src/admin/actions.ts | 8 +- src/admin/insights/AdminAppInsights.tsx | 6 + src/admin/insights/AdminAppInsightsClient.tsx | 35 +++- src/admin/insights/InsightsIndicatorDot.tsx | 4 +- src/admin/insights/index.ts | 61 ++++-- src/admin/insights/server.ts | 20 +- src/app/config.ts | 3 + src/components/EnvVar.tsx | 19 +- src/components/cmdk/CommandKClient.tsx | 4 +- src/photo/PhotoGridSidebar.tsx | 197 ++++++++++-------- src/state/AppState.ts | 6 +- src/state/AppStateProvider.tsx | 12 +- 16 files changed, 237 insertions(+), 158 deletions(-) diff --git a/README.md b/README.md index 5946b518..6cec6f2e 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,7 @@ Application behavior can be changed by configuring the following environment var - `NEXT_PUBLIC_HIDE_FILM_SIMULATIONS = 1` prevents Fujifilm simulations showing up in `/grid` sidebar and CMD-K search results - `NEXT_PUBLIC_HIDE_RECIPES = 1` prevents Fujifilm recipe button showing up in photo meta - `NEXT_PUBLIC_HIDE_REPO_LINK = 1` removes footer link to repo +- `NEXT_PUBLIC_CAMERAS_FIRST = 1` shows cameras above tags in grid sidebar #### Grid - `NEXT_PUBLIC_GRID_HOMEPAGE = 1` shows grid layout on homepage @@ -143,7 +144,6 @@ Application behavior can be changed by configuring the following environment var - `NEXT_PUBLIC_ALLOW_PUBLIC_DOWNLOADS = 1` enables public photo downloads for all visitors (⚠️ may result in increased bandwidth usage) - `NEXT_PUBLIC_PUBLIC_API = 1` enables public API available at `/api` - `NEXT_PUBLIC_IGNORE_PRIORITY_ORDER = 1` prevents `priority_order` field affecting photo order -- `NEXT_PUBLIC_SHOW_LARGE_THUMBNAILS = 1` ensures large thumbnails on photo grid views - `NEXT_PUBLIC_OG_TEXT_ALIGNMENT = BOTTOM` keeps OG image text bottom aligned (default is top) ## Alternate storage providers diff --git a/src/admin/AdminAppConfigurationClient.tsx b/src/admin/AdminAppConfigurationClient.tsx index 87f827bb..283f36c5 100644 --- a/src/admin/AdminAppConfigurationClient.tsx +++ b/src/admin/AdminAppConfigurationClient.tsx @@ -75,6 +75,7 @@ export default function AdminAppConfigurationClient({ showFilmSimulations, showRecipes, showRepoLink, + showSidebarCamerasFirst, // Grid isGridHomepageEnabled, gridAspectRatio, @@ -543,6 +544,15 @@ export default function AdminAppConfigurationClient({ Set environment variable to {'"1"'} to hide footer link: {renderEnvVars(['NEXT_PUBLIC_HIDE_REPO_LINK'])} + + Set environment variable to {'"1"'} to show cameras + above tags in grid sidebar: + {renderEnvVars(['NEXT_PUBLIC_CAMERAS_FIRST'])} + - {insightIndicatorStatus && + {insightsIndicatorStatus && 1; - const { insightIndicatorStatus } = useAppState(); + const { insightsIndicatorStatus } = useAppState(); return (
@@ -65,7 +65,7 @@ export default function AdminInfoPage({ {title} - {title === 'App Insights' && insightIndicatorStatus && + {title === 'App Insights' && insightsIndicatorStatus && runAuthenticatedAdminServerAction(async () => { @@ -17,7 +17,7 @@ export const getAdminDataAction = async () => countHiddenPhotos, countTags, countUploads, - shouldShowInsightsIndicator, + insightsIndicatorStatus, ] = await Promise.all([ getPhotosMetaCached() .then(({ count }) => count) @@ -34,7 +34,7 @@ export const getAdminDataAction = async () => console.error(`Error getting blob upload urls: ${e}`); return 0; }), - getShouldShowInsightsIndicator(), + getInsightsIndicatorStatus(), ]); return { @@ -42,7 +42,7 @@ export const getAdminDataAction = async () => countHiddenPhotos, countTags, countUploads, - shouldShowInsightsIndicator, + insightsIndicatorStatus, }; }); diff --git a/src/admin/insights/AdminAppInsights.tsx b/src/admin/insights/AdminAppInsights.tsx index 122eee33..36077d9d 100644 --- a/src/admin/insights/AdminAppInsights.tsx +++ b/src/admin/insights/AdminAppInsights.tsx @@ -11,11 +11,13 @@ import { GRID_HOMEPAGE_ENABLED, HAS_STATIC_OPTIMIZATION, MATTE_PHOTOS, + SHOW_SIDEBAR_CAMERAS_FIRST, } from '@/app/config'; import { getGitHubMetaForCurrentApp, getSignificantInsights } from '.'; import { getOutdatedPhotosCount } from '@/photo/db/query'; const BASIC_PHOTO_INSTALLATION_COUNT = 32; +const TAG_COUNT_THRESHOLD = 12; export default async function AdminAppInsights() { const [ @@ -63,6 +65,10 @@ export default async function AdminAppInsights() { noConfiguredDomain, outdatedPhotos, photoMatting: photosCountPortrait > 0 && !MATTE_PHOTOS, + camerasFirst: ( + tags.length > TAG_COUNT_THRESHOLD && + !SHOW_SIDEBAR_CAMERAS_FIRST + ), gridFirst: ( photosCount >= BASIC_PHOTO_INSTALLATION_COUNT && !GRID_HOMEPAGE_ENABLED diff --git a/src/admin/insights/AdminAppInsightsClient.tsx b/src/admin/insights/AdminAppInsightsClient.tsx index 855c0599..973b4285 100644 --- a/src/admin/insights/AdminAppInsightsClient.tsx +++ b/src/admin/insights/AdminAppInsightsClient.tsx @@ -7,6 +7,7 @@ import PhotoFilmSimulationIcon from '@/simulation/PhotoFilmSimulationIcon'; import { FaCamera } from 'react-icons/fa'; import { FaTag } from 'react-icons/fa'; import { FaCircleInfo, FaRegCalendar } from 'react-icons/fa6'; +import { HiMiniArrowsUpDown } from 'react-icons/hi2'; import { HiOutlinePhotograph } from 'react-icons/hi'; import { MdAspectRatio } from 'react-icons/md'; import { PiWarningBold } from 'react-icons/pi'; @@ -107,6 +108,7 @@ export default function AdminAppInsightsClient({ noConfiguredDomain, outdatedPhotos, photoMatting, + camerasFirst, gridFirst, noStaticOptimization, } = insights; @@ -271,7 +273,10 @@ export default function AdminAppInsightsClient({ Not explicitly setting a domain may cause certain features to behave unexpectedly. Domains are stored in {' '} - . + } />} {(noStaticOptimization || debug) && Enable automatic AI text generation {' '} - by setting . + by setting {' '} Further instruction and cost considerations in {' '} @@ -331,7 +339,28 @@ export default function AdminAppInsightsClient({ {' '} portrait and landscape photos appear more consistent {' '} - . + + } + />} + {(camerasFirst || debug) && } + content="Move cameras above tags in sidebar" + expandContent={<> + Now that you have more than a few tags, consider + showing cameras first in the sidebar by setting + {' '} + } />} {(gridFirst || debug) && { switch (size) { @@ -39,7 +39,7 @@ export default function InsightsIndicatorDot({ bottom !== undefined || left !== undefined ) && 'absolute', - (colorOverride ?? insightIndicatorStatus) === 'blue' + (colorOverride ?? insightsIndicatorStatus) === 'blue' ? 'text-blue-500' : 'text-amber-500', className, diff --git a/src/admin/insights/index.ts b/src/admin/insights/index.ts index 9b9b6e34..2a23fffd 100644 --- a/src/admin/insights/index.ts +++ b/src/admin/insights/index.ts @@ -10,41 +10,39 @@ import { import { PhotoDateRange } from '@/photo'; import { getGitHubMeta } from '@/platforms/github'; -type AdminAppInsightCode = - 'noFork' | - 'forkBehind'; +const AdminAppInsightCode = [ + 'noFork', + 'forkBehind', +] as const; +type AdminAppInsightCode = typeof AdminAppInsightCode[number]; -type AdminAppInsightRecommendation = - 'noAi' | - 'noAiRateLimiting' | - 'noConfiguredDomain' | - 'photoMatting' | - 'gridFirst' | - 'noStaticOptimization'; +const _INSIGHTS_TEMPLATE = [ + 'noAi', + 'noAiRateLimiting', + 'noConfiguredDomain', + 'photoMatting', + 'camerasFirst', + 'gridFirst', + 'noStaticOptimization', +] as const; +type AdminAppInsightRecommendation = typeof _INSIGHTS_TEMPLATE[number]; -type AdminAppInsightLibrary = - 'outdatedPhotos'; +const _INSIGHTS_LIBRARY = [ + 'outdatedPhotos', +] as const; +type AdminAppInsightLibrary = typeof _INSIGHTS_LIBRARY[number]; export type AdminAppInsight = AdminAppInsightCode | AdminAppInsightRecommendation | AdminAppInsightLibrary; -const RECOMMENDATIONS: AdminAppInsightRecommendation[] = [ - 'noAi', - 'noAiRateLimiting', - 'noConfiguredDomain', - 'photoMatting', - 'gridFirst', - 'noStaticOptimization', -]; - export type AdminAppInsights = Record -export type InsightIndicatorStatus = 'blue' | 'yellow' | undefined; +export type InsightsIndicatorStatus = 'blue' | 'yellow' | undefined; export const hasTemplateRecommendations = (insights: AdminAppInsights) => - RECOMMENDATIONS.some(insight => insights[insight]); + _INSIGHTS_TEMPLATE.some(insight => insights[insight]); export interface PhotoStats { photosCount: number @@ -87,3 +85,20 @@ export const getSignificantInsights = ({ outdatedPhotos: Boolean(photosCountOutdated), }; }; + +export const indicatorStatusForSignificantInsights = ( + insights: Awaited>, +) => { + const { + forkBehind, + noAiRateLimiting, + noConfiguredDomain, + outdatedPhotos, + } = insights; + + if (noAiRateLimiting || noConfiguredDomain) { + return 'yellow'; + } else if (forkBehind || outdatedPhotos) { + return 'blue'; + } +}; diff --git a/src/admin/insights/server.ts b/src/admin/insights/server.ts index 5090b12f..3b453def 100644 --- a/src/admin/insights/server.ts +++ b/src/admin/insights/server.ts @@ -1,8 +1,11 @@ import { getOutdatedPhotosCount } from '@/photo/db/query'; -import { getSignificantInsights } from '.'; +import { + getSignificantInsights, + indicatorStatusForSignificantInsights, +} from '.'; import { getGitHubMetaForCurrentApp } from '.'; -export const getShouldShowInsightsIndicator = async () => { +export const getInsightsIndicatorStatus = async () => { const [ codeMeta, photosCountOutdated, @@ -11,19 +14,10 @@ export const getShouldShowInsightsIndicator = async () => { getOutdatedPhotosCount(), ]); - const { - forkBehind, - noAiRateLimiting, - noConfiguredDomain, - outdatedPhotos, - } = getSignificantInsights({ + const significantInsights = getSignificantInsights({ codeMeta, photosCountOutdated, }); - if (noAiRateLimiting || noConfiguredDomain) { - return 'yellow'; - } else if (forkBehind || outdatedPhotos) { - return 'blue'; - } + return indicatorStatusForSignificantInsights(significantInsights); }; diff --git a/src/app/config.ts b/src/app/config.ts index 4fb01969..0fca306c 100644 --- a/src/app/config.ts +++ b/src/app/config.ts @@ -221,6 +221,8 @@ export const SHOW_RECIPES = process.env.NEXT_PUBLIC_HIDE_RECIPES !== '1'; export const SHOW_REPO_LINK = process.env.NEXT_PUBLIC_HIDE_REPO_LINK !== '1'; +export const SHOW_SIDEBAR_CAMERAS_FIRST = + process.env.NEXT_PUBLIC_CAMERAS_FIRST === '1'; // GRID @@ -317,6 +319,7 @@ export const APP_CONFIGURATION = { showFilmSimulations: SHOW_FILM_SIMULATIONS, showRecipes: SHOW_RECIPES, showRepoLink: SHOW_REPO_LINK, + showSidebarCamerasFirst: SHOW_SIDEBAR_CAMERAS_FIRST, // Grid isGridHomepageEnabled: GRID_HOMEPAGE_ENABLED, gridAspectRatio: GRID_ASPECT_RATIO, diff --git a/src/components/EnvVar.tsx b/src/components/EnvVar.tsx index b96ecf3b..44bd1158 100644 --- a/src/components/EnvVar.tsx +++ b/src/components/EnvVar.tsx @@ -34,13 +34,18 @@ export default function EnvVar({ {variable}{value && ` = ${value}`} {includeCopyButton && - } - {trailingContent} + + + } + {trailingContent && + + {trailingContent} + }
); diff --git a/src/components/cmdk/CommandKClient.tsx b/src/components/cmdk/CommandKClient.tsx index 3bf4bd5d..e249b48f 100644 --- a/src/components/cmdk/CommandKClient.tsx +++ b/src/components/cmdk/CommandKClient.tsx @@ -108,7 +108,7 @@ export default function CommandKClient({ tagsCount, selectedPhotoIds, setSelectedPhotoIds, - insightIndicatorStatus, + insightsIndicatorStatus, isGridHighDensity, areZoomControlsShown, arePhotosMatted, @@ -365,7 +365,7 @@ export default function CommandKClient({ adminSection.items.push({ label: App Insights - {insightIndicatorStatus && + {insightsIndicatorStatus && } , keywords: ['app insights'], diff --git a/src/photo/PhotoGridSidebar.tsx b/src/photo/PhotoGridSidebar.tsx index 2288d94a..b8ca861e 100644 --- a/src/photo/PhotoGridSidebar.tsx +++ b/src/photo/PhotoGridSidebar.tsx @@ -15,7 +15,7 @@ import FavsTag from '../tag/FavsTag'; import { useAppState } from '@/state/AppState'; import { useMemo } from 'react'; import HiddenTag from '@/tag/HiddenTag'; -import { SITE_ABOUT } from '@/app/config'; +import { SHOW_SIDEBAR_CAMERAS_FIRST, SITE_ABOUT } from '@/app/config'; import { htmlHasBrParagraphBreaks, safelyParseFormattedHtml, @@ -43,6 +43,107 @@ export default function PhotoGridSidebar({ addHiddenToTags(tags, photosCountHidden) , [tags, photosCountHidden]); + const tagsContent = tags.length > 0 + ? } + items={tagsIncludingHidden.map(({ tag, count }) => { + switch (tag) { + case TAG_FAVS: + return ; + case TAG_HIDDEN: + return ; + default: + return ; + } + })} + /> + : null; + + const camerasContent = cameras.length > 0 + ? } + items={cameras + .sort(sortCamerasWithCount) + .map(({ cameraKey, camera, count }) => + )} + /> + : null; + + const filmsContent = simulations.length > 0 + ? } + items={simulations + .sort(sortFilmSimulationsWithCount) + .map(({ simulation, count }) => +
+ +
)} + /> + : null; + + const photoStatsContent = photosCount > 0 + ? start + ? + : + : null; + return (
{SITE_ABOUT && ]} />} - {tags.length > 0 && } - items={tagsIncludingHidden.map(({ tag, count }) => { - switch (tag) { - case TAG_FAVS: - return ; - case TAG_HIDDEN: - return ; - default: - return ; - } - })} - />} - {cameras.length > 0 && } - items={cameras - .sort(sortCamerasWithCount) - .map(({ cameraKey, camera, count }) => - )} - />} - {simulations.length > 0 && } - items={simulations - .sort(sortFilmSimulationsWithCount) - .map(({ simulation, count }) => -
- -
)} - />} - {photosCount > 0 && start - ? - : } + {SHOW_SIDEBAR_CAMERAS_FIRST + ? <>{camerasContent}{tagsContent} + : <>{tagsContent}{camerasContent}} + {filmsContent} + {photoStatsContent}
); } diff --git a/src/state/AppState.ts b/src/state/AppState.ts index daa77591..1da924ec 100644 --- a/src/state/AppState.ts +++ b/src/state/AppState.ts @@ -7,7 +7,7 @@ import { } from 'react'; import { AnimationConfig } from '@/components/AnimateItems'; import { ShareModalProps } from '@/share'; -import { InsightIndicatorStatus } from '@/admin/insights'; +import { InsightsIndicatorStatus } from '@/admin/insights'; import { INITIAL_UPLOAD_STATE, UploadState } from '@/admin/upload'; export interface AppStateContext { @@ -52,8 +52,8 @@ export interface AppStateContext { setSelectedPhotoIds?: Dispatch> isPerformingSelectEdit?: boolean setIsPerformingSelectEdit?: Dispatch> - insightIndicatorStatus?: InsightIndicatorStatus - setInsightIndicatorStatus?: Dispatch> + insightsIndicatorStatus?: InsightsIndicatorStatus + setInsightsIndicatorStatus?: Dispatch> // DEBUG isGridHighDensity?: boolean setIsGridHighDensity?: Dispatch> diff --git a/src/state/AppStateProvider.tsx b/src/state/AppStateProvider.tsx index 7fdb795d..36eaa49d 100644 --- a/src/state/AppStateProvider.tsx +++ b/src/state/AppStateProvider.tsx @@ -14,7 +14,7 @@ import { } from '@/app/config'; import { ShareModalProps } from '@/share'; import { storeTimezoneCookie } from '@/utility/timezone'; -import { InsightIndicatorStatus } from '@/admin/insights'; +import { InsightsIndicatorStatus } from '@/admin/insights'; import { getAdminDataAction } from '@/admin/actions'; import { storeAuthEmailCookie, @@ -72,8 +72,8 @@ export default function AppStateProvider({ useState(); const [isPerformingSelectEdit, setIsPerformingSelectEdit] = useState(false); - const [insightIndicatorStatus, setInsightIndicatorStatus] = - useState(); + const [insightsIndicatorStatus, setInsightsIndicatorStatus] = + useState(); // DEBUG const [isGridHighDensity, setIsGridHighDensity] = useState(HIGH_DENSITY_GRID); @@ -138,7 +138,7 @@ export default function AppStateProvider({ setPhotosCountHidden(adminData.countHiddenPhotos); setUploadsCount(adminData.countUploads); setTagsCount(adminData.countTags); - setInsightIndicatorStatus(adminData.shouldShowInsightsIndicator); + setInsightsIndicatorStatus(adminData.insightsIndicatorStatus); } } else { setPhotosCountHidden(0); @@ -205,8 +205,8 @@ export default function AppStateProvider({ setSelectedPhotoIds, isPerformingSelectEdit, setIsPerformingSelectEdit, - insightIndicatorStatus, - setInsightIndicatorStatus, + insightsIndicatorStatus, + setInsightsIndicatorStatus, // DEBUG isGridHighDensity, setIsGridHighDensity,