From e1082a8a3d2be0e4c52209e95f21a8005d5ecee6 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Fri, 14 Feb 2025 18:06:53 -0600 Subject: [PATCH] Finalize core insights UX --- src/admin/AdminOutdatedClient.tsx | 1 + src/admin/insights/AdminAppInsights.tsx | 7 +- src/admin/insights/AdminAppInsightsClient.tsx | 260 ++++++++++++------ src/admin/insights/index.ts | 36 +++ src/app-core/config.ts | 3 + src/components/EnvVar.tsx | 1 + src/components/LinkWithStatus.tsx | 2 +- src/components/ScoreCardRow.tsx | 8 +- 8 files changed, 233 insertions(+), 85 deletions(-) diff --git a/src/admin/AdminOutdatedClient.tsx b/src/admin/AdminOutdatedClient.tsx index 2ee82d62..210b025f 100644 --- a/src/admin/AdminOutdatedClient.tsx +++ b/src/admin/AdminOutdatedClient.tsx @@ -67,6 +67,7 @@ export default function AdminOutdatedClient({ } }} isLoading={arePhotoIdsSyncing} + disabled={!updateBatchSize} > {arePhotoIdsSyncing ? 'Syncing' diff --git a/src/admin/insights/AdminAppInsights.tsx b/src/admin/insights/AdminAppInsights.tsx index e4fa2a47..c951ce77 100644 --- a/src/admin/insights/AdminAppInsights.tsx +++ b/src/admin/insights/AdminAppInsights.tsx @@ -19,6 +19,7 @@ import { VERCEL_GIT_REPO_SLUG, } from '@/app-core/config'; import { getGitHubMetaWithFallback } from '../github'; +import { OUTDATED_THRESHOLD } from '@/photo'; const BASIC_PHOTO_INSTALLATION_COUNT = 32; @@ -31,6 +32,7 @@ export default async function AdminAppInsights() { const [ { count: photosCount, dateRange }, { count: photosCountHidden }, + { count: photosCountOutdated }, { count: photosCountPortrait }, tags, cameras, @@ -39,6 +41,7 @@ export default async function AdminAppInsights() { ] = await Promise.all([ getPhotosMeta({ hidden: 'include' }), getPhotosMeta({ hidden: 'only' }), + getPhotosMeta({ hidden: 'include', updatedBefore: OUTDATED_THRESHOLD }), getPhotosMeta({ maximumAspectRatio: 0.9 }), getUniqueTags(), getUniqueCameras(), @@ -63,11 +66,12 @@ export default async function AdminAppInsights() { return ( 0 && !MATTE_PHOTOS, gridFirst: ( photosCount >= BASIC_PHOTO_INSTALLATION_COUNT && @@ -78,6 +82,7 @@ export default async function AdminAppInsights() { photoStats={{ photosCount, photosCountHidden, + photosCountOutdated, tagsCount: tags.length, camerasCount: cameras.length, filmSimulationsCount: filmSimulations.length, diff --git a/src/admin/insights/AdminAppInsightsClient.tsx b/src/admin/insights/AdminAppInsightsClient.tsx index ee7a1082..47e35a93 100644 --- a/src/admin/insights/AdminAppInsightsClient.tsx +++ b/src/admin/insights/AdminAppInsightsClient.tsx @@ -1,20 +1,16 @@ 'use client'; -import IconGrSync from '@/app-core/IconGrSync'; import ScoreCard from '@/components/ScoreCard'; import ScoreCardRow from '@/components/ScoreCardRow'; -import { dateRangeForPhotos, PhotoDateRange } from '@/photo'; +import { dateRangeForPhotos } from '@/photo'; import PhotoFilmSimulationIcon from '@/simulation/PhotoFilmSimulationIcon'; import { FaCamera } from 'react-icons/fa'; import { FaTag } from 'react-icons/fa'; -import { FaRegCalendar } from 'react-icons/fa6'; -import { - HiOutlinePhotograph, - HiSparkles, -} from 'react-icons/hi'; -import { MdLightbulbOutline } from 'react-icons/md'; +import { FaCircleInfo, FaRegCalendar } from 'react-icons/fa6'; +import { HiOutlinePhotograph } from 'react-icons/hi'; +import { MdAspectRatio } from 'react-icons/md'; import { PiWarningBold } from 'react-icons/pi'; -import { TbCone } from 'react-icons/tb'; +import { TbCone, TbSparkles } from 'react-icons/tb'; import { getGitHubMetaWithFallback } from '../github'; import { BiGitBranch, BiGitCommit, BiLogoGithub } from 'react-icons/bi'; import { @@ -23,24 +19,40 @@ import { TEMPLATE_REPO_NAME, VERCEL_GIT_COMMIT_SHA_SHORT, VERCEL_GIT_COMMIT_MESSAGE, + TEMPLATE_REPO_URL_FORK, + TEMPLATE_REPO_URL_README, } from '@/app-core/config'; -import { AdminAppInsight } from '.'; +import { AdminAppInsights, hasTemplateRecommendations, PhotoStats } from '.'; import EnvVar from '@/components/EnvVar'; import { IoSyncCircle } from 'react-icons/io5'; import clsx from 'clsx/lite'; +import { PATH_ADMIN_OUTDATED } from '@/app-core/paths'; +import { LiaBroomSolid } from 'react-icons/lia'; +import { IoMdGrid } from 'react-icons/io'; +import { RiSpeedMiniLine } from 'react-icons/ri'; +import LinkWithStatus from '@/components/LinkWithStatus'; + +const readmeAnchor = (anchor: string, text: string) => + + {text} + ; const DEBUG_COMMIT_SHA = '4cd29ed'; const DEBUG_COMMIT_MESSAGE = 'Long commit message for debugging purposes'; +const DEBUG_BEHIND_BY = 9; +const DEBUG_PHOTOS_COUNT_OUTDATED = 7; export default function AdminAppInsightsClient({ codeMeta, - recommendations: { - noAi, - noAiRateLimiting, - }, + insights, photoStats: { photosCount, photosCountHidden, + photosCountOutdated, tagsCount, camerasCount, filmSimulationsCount, @@ -50,18 +62,20 @@ export default function AdminAppInsightsClient({ debug, }: { codeMeta?: Awaited> - recommendations: Record - photoStats: { - photosCount: number - photosCountHidden: number - tagsCount: number - camerasCount: number - filmSimulationsCount: number - lensesCount: number - dateRange?: PhotoDateRange - }, + insights: AdminAppInsights + photoStats: PhotoStats debug?: boolean }) { + const { + noFork, + forkBehind, + noAi, + noAiRateLimiting, + outdatedPhotos, + photoMatting, + gridFirst, + noStaticOptimization, + } = insights; const { descriptionWithSpaces } = dateRangeForPhotos(undefined, dateRange); @@ -69,37 +83,63 @@ export default function AdminAppInsightsClient({
{(codeMeta?.isBaseRepo || codeMeta?.isForkedFromBase || debug) && <> - {(codeMeta?.behindBy || debug) && + {(noFork || debug) && } - content={<> - This fork is - {' '} - - {codeMeta?.behindBy ?? 9} commits - - {' '} - behind - } - additionalContent={<> + content="This template is not forked" + expandContent={<> - Sync your fork + Fork {' '} - to receive the latest fixes and features + original template to receive the latest fixes and features. + {' '} + {readmeAnchor('receiving-updates', 'Additional instructions')} + {' '} + in README. } />} + {(forkBehind || debug) && } + content={<> + This fork is + {' '} + + {codeMeta?.behindBy ?? DEBUG_BEHIND_BY} + {' '} + {(codeMeta?.behindBy ?? DEBUG_BEHIND_BY) === 1 + ? 'commit' + : 'commits'} + + {' '} + behind + } + expandContent={<> + + Sync your fork + + {' '} + to receive the latest fixes and features + } + />} } content={
} - - {(noAiRateLimiting || debug) && + {(noAiRateLimiting || debug) && } + content="AI enabled without rate limiting" + // eslint-disable-next-line max-len + expandContent="Create Vercel KV store and link o this project in order to enable rate limiting." />} - content="AI enabled without rate limiting" - // eslint-disable-next-line max-len - additionalContent="Create Vercel KV store and link o this project in order to enable rate limiting." - />} - {(noAi || debug) && } - content="Enable AI text generation to improve photo descriptions" - // eslint-disable-next-line max-len - additionalContent="Create Vercel KV store and link it to this project in order to enable rate limiting." - />} - } - // eslint-disable-next-line max-len - content="You seem to have several vertical photos—consider enabling matting to make portrait and landscape photos appear more consistent" - additionalContent={<> - Enabled photo matting by setting - - } - /> - } - // eslint-disable-next-line max-len - content="Consider forking this repository to receive new features and fixes" - /> - } - content="Enable AI text generation in the app configuration" - /> - + {(noAi || debug) && } + content="Improve SEO + accessibility with AI" + expandContent={<> +
+ Enable automatic AI text generation + {' '} + by setting environment variable + {' '} + . +
+
+ Further instruction in + {' '} + {readmeAnchor('ai-text-generation', 'README')}. +
+ } + />} + {(photoMatting || debug) && } + content="Vertical photos may benefit from matting" + expandContent={<> + {/* eslint-disable-next-line max-len */} + Enable photo matting to make portrait and landscape photos appear more consistent + + } + />} + {(gridFirst || debug) && } + content="Grid homepage" + expandContent={<> + Enable grid homepage by setting environment variable + {' '} + + } + />} + {(noStaticOptimization || debug) && } + content="Static optimization" + expandContent={<> + {/* eslint-disable-next-line max-len */} + Enable static optimization by setting any of the following environment variables: +
+ + + + +
+ } + />} + } + {(outdatedPhotos || debug) && } + // eslint-disable-next-line max-len + content={`You have ${photosCountOutdated || DEBUG_PHOTOS_COUNT_OUTDATED} outdated ${(photosCountOutdated || DEBUG_PHOTOS_COUNT_OUTDATED) === 1 ? 'photo' : 'photos'}`} + expandContent={<> + + View outdated photos + + {' '} + to update them in batches. + } + />} + +export const hasTemplateRecommendations = (insights: AdminAppInsights) => + RECOMMENDATIONS.some(insight => insights[insight]); + +export interface PhotoStats { + photosCount: number + photosCountHidden: number + photosCountOutdated: number + tagsCount: number + camerasCount: number + filmSimulationsCount: number + lensesCount: number + dateRange?: PhotoDateRange +} + +export const getInsightIndicator = ({ + forkBehind, + noAiRateLimiting, + outdatedPhotos, +}: AdminAppInsights) => + forkBehind || + noAiRateLimiting || + outdatedPhotos; diff --git a/src/app-core/config.ts b/src/app-core/config.ts index 5dfe1a30..7dbdf6c9 100644 --- a/src/app-core/config.ts +++ b/src/app-core/config.ts @@ -21,6 +21,9 @@ export const TEMPLATE_REPO_NAME = 'exif-photo-blog'; export const TEMPLATE_REPO_BRANCH = 'main'; // eslint-disable-next-line max-len export const TEMPLATE_REPO_URL = `https://github.com/${TEMPLATE_REPO_OWNER}/${TEMPLATE_REPO_NAME}`; +export const TEMPLATE_REPO_URL_FORK = `${TEMPLATE_REPO_URL}/fork`; +// eslint-disable-next-line max-len +export const TEMPLATE_REPO_URL_README = `${TEMPLATE_REPO_URL}?tab=readme-ov-file`; export const VERCEL_GIT_PROVIDER = process.env.NEXT_PUBLIC_VERCEL_GIT_PROVIDER; diff --git a/src/components/EnvVar.tsx b/src/components/EnvVar.tsx index 5e45c348..710d37ee 100644 --- a/src/components/EnvVar.tsx +++ b/src/components/EnvVar.tsx @@ -23,6 +23,7 @@ export default function EnvVar({ 'px-1.5 py-[0.5px]', 'rounded-md', 'bg-gray-100 dark:bg-gray-800', + 'whitespace-nowrap', )}> {variable}{value && ` = ${value}`} diff --git a/src/components/LinkWithStatus.tsx b/src/components/LinkWithStatus.tsx index a2ddf0a9..fa5a2803 100644 --- a/src/components/LinkWithStatus.tsx +++ b/src/components/LinkWithStatus.tsx @@ -89,7 +89,7 @@ export default function LinkWithStatus({ {...props } href={href} className={clsx( - 'relative flex transition-[colors,opacity]', + 'relative inline-flex transition-[colors,opacity]', (loadingClassName || isControlled) ? 'opacity-100' : isLoading ? 'opacity-50' : 'opacity-100', diff --git a/src/components/ScoreCardRow.tsx b/src/components/ScoreCardRow.tsx index 0e8a2a4f..21f0a31a 100644 --- a/src/components/ScoreCardRow.tsx +++ b/src/components/ScoreCardRow.tsx @@ -5,12 +5,12 @@ import { LuChevronsDownUp, LuChevronsUpDown } from 'react-icons/lu'; export default function ScoreCardRow({ icon, content, - additionalContent, + expandContent, className, }: { icon: ReactNode content: ReactNode - additionalContent?: ReactNode + expandContent?: ReactNode className?: string }) { const [isExpanded, setIsExpanded] = useState(false); @@ -33,10 +33,10 @@ export default function ScoreCardRow({
{isExpanded &&
- {additionalContent} + {expandContent}
}
- {additionalContent &&