Simplify About page to title, subhead, and description only.
Remove avatar/hero photos and gallery stats from the public About view and admin editor, and add env-backed defaults for ABOUT_TITLE and ABOUT_SUBHEAD. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
2315647743
commit
8e6df1e70c
@ -1,53 +1,9 @@
|
||||
import AdminAboutEditPage from '@/about/AdminAboutEditPage';
|
||||
import { getAboutData } from '@/about/data';
|
||||
import { PRESERVE_ORIGINAL_UPLOADS } from '@/app/config';
|
||||
import { feedQueryOptions } from '@/feed';
|
||||
import {
|
||||
getPhotosCached,
|
||||
getPhotosMetaCached,
|
||||
} from '@/photo/cache';
|
||||
import { TAG_FAVS } from '@/tag';
|
||||
|
||||
const PHOTO_CHOOSER_QUERY_OPTIONS = feedQueryOptions({
|
||||
isGrid: true,
|
||||
excludeFromFeeds: false,
|
||||
});
|
||||
|
||||
export default async function AboutEditPage() {
|
||||
const [
|
||||
{
|
||||
about,
|
||||
photoAvatar,
|
||||
photoHero,
|
||||
},
|
||||
photos,
|
||||
photosCount,
|
||||
photosFavs,
|
||||
] = await Promise.all([
|
||||
getAboutData()
|
||||
.catch(() => ({
|
||||
about: undefined,
|
||||
photoAvatar: undefined,
|
||||
photoHero: undefined,
|
||||
})),
|
||||
getPhotosCached(PHOTO_CHOOSER_QUERY_OPTIONS)
|
||||
.catch(() => []),
|
||||
getPhotosMetaCached(PHOTO_CHOOSER_QUERY_OPTIONS)
|
||||
.then(({ count }) => count)
|
||||
.catch(() => 0),
|
||||
getPhotosCached({ tag: TAG_FAVS })
|
||||
.catch(() => []),
|
||||
]);
|
||||
const { about } = await getAboutData()
|
||||
.catch(() => ({ about: undefined }));
|
||||
|
||||
return (
|
||||
<AdminAboutEditPage {...{
|
||||
about,
|
||||
photoAvatar,
|
||||
photoHero,
|
||||
photos,
|
||||
photosCount,
|
||||
photosFavs,
|
||||
shouldResizeImages: !PRESERVE_ORIGINAL_UPLOADS,
|
||||
}} />
|
||||
);
|
||||
return <AdminAboutEditPage about={about} />;
|
||||
}
|
||||
|
||||
@ -1,18 +1,13 @@
|
||||
import AboutPageClient from '@/about/AboutPageClient';
|
||||
import { getAboutDataCached } from '@/about/data';
|
||||
import { ABOUT_DESCRIPTION_DEFAULT, SHOW_ABOUT_PAGE } from '@/app/config';
|
||||
import { PATH_ROOT } from '@/app/path';
|
||||
import { getDataForCategoriesCached } from '@/category/cache';
|
||||
import {
|
||||
getLastModifiedForCategories,
|
||||
NULL_CATEGORY_DATA,
|
||||
} from '@/category/data';
|
||||
import { getPhotosMetaCached } from '@/photo/cache';
|
||||
import PhotosEmptyState from '@/photo/PhotosEmptyState';
|
||||
import { getAllPhotoIdsWithUpdatedAt } from '@/photo/query';
|
||||
import { TAG_FAVS } from '@/tag';
|
||||
ABOUT_DESCRIPTION_DEFAULT,
|
||||
ABOUT_SUBHEAD,
|
||||
ABOUT_TITLE,
|
||||
SHOW_ABOUT_PAGE,
|
||||
} from '@/app/config';
|
||||
import { PATH_ROOT } from '@/app/path';
|
||||
import { safelyParseFormattedHtml } from '@/utility/html';
|
||||
import { max } from 'date-fns';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
export const dynamic = 'force-static';
|
||||
@ -20,75 +15,27 @@ export const dynamic = 'force-static';
|
||||
export default async function AboutPage() {
|
||||
if (!SHOW_ABOUT_PAGE) { redirect(PATH_ROOT); }
|
||||
|
||||
const [
|
||||
{
|
||||
about,
|
||||
photoAvatar,
|
||||
photoHero,
|
||||
},
|
||||
photosMeta,
|
||||
photos,
|
||||
categories,
|
||||
] = await Promise.all([
|
||||
getAboutDataCached()
|
||||
.catch(() => ({
|
||||
about: undefined,
|
||||
photoAvatar: undefined,
|
||||
photoHero: undefined,
|
||||
})),
|
||||
getPhotosMetaCached().catch(() => {}),
|
||||
getAllPhotoIdsWithUpdatedAt().catch(() => []),
|
||||
getDataForCategoriesCached().catch(() => (NULL_CATEGORY_DATA)),
|
||||
]);
|
||||
const { about } = await getAboutDataCached()
|
||||
.catch(() => ({ about: undefined }));
|
||||
|
||||
const title = about?.title || ABOUT_TITLE;
|
||||
const subhead = about?.subhead || ABOUT_SUBHEAD;
|
||||
const description = about?.description || ABOUT_DESCRIPTION_DEFAULT;
|
||||
|
||||
const descriptionHtml = description
|
||||
? <div
|
||||
className="text-medium [&>*>a]:underline"
|
||||
className="text-medium leading-relaxed [&>*>a]:underline space-y-4"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: safelyParseFormattedHtml(description),
|
||||
}}
|
||||
/>
|
||||
: undefined;
|
||||
|
||||
const {
|
||||
cameras,
|
||||
lenses,
|
||||
albums,
|
||||
tags,
|
||||
recipes,
|
||||
films,
|
||||
} = categories;
|
||||
|
||||
const place = albums
|
||||
.slice()
|
||||
.sort((a, b) => b.count - a.count)[0]?.album.location;
|
||||
|
||||
const lastModifiedSite = max([
|
||||
getLastModifiedForCategories(categories, photos),
|
||||
about?.updatedAt,
|
||||
].filter(date => date instanceof Date));
|
||||
|
||||
return (
|
||||
(photosMeta?.count ?? 0) > 0
|
||||
? <AboutPageClient
|
||||
title={about?.title}
|
||||
subhead={about?.subhead}
|
||||
descriptionHtml={descriptionHtml}
|
||||
photosCount={photosMeta?.count}
|
||||
photosOldest={photosMeta?.dateRange?.start}
|
||||
photoAvatar={photoAvatar}
|
||||
photoHero={photoHero}
|
||||
camera={cameras[0]?.camera}
|
||||
lens={lenses[0]?.lens}
|
||||
recipe={recipes[0]?.recipe}
|
||||
film={films[0]?.film}
|
||||
tag={tags.filter(({ tag }) => tag !== TAG_FAVS)[0]?.tag}
|
||||
place={place}
|
||||
album={albums[0]?.album}
|
||||
lastUpdated={lastModifiedSite}
|
||||
/>
|
||||
: <PhotosEmptyState />
|
||||
<AboutPageClient
|
||||
title={title}
|
||||
subhead={subhead}
|
||||
descriptionHtml={descriptionHtml}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,194 +1,45 @@
|
||||
'use client';
|
||||
|
||||
import PhotoAlbum from '@/album/PhotoAlbum';
|
||||
import { useAppState } from '@/app/AppState';
|
||||
import PhotoCamera from '@/camera/PhotoCamera';
|
||||
import AnimateItems from '@/components/AnimateItems';
|
||||
import AppGrid from '@/components/AppGrid';
|
||||
import PhotoFilm from '@/film/PhotoFilm';
|
||||
import PhotoLens from '@/lens/PhotoLens';
|
||||
import { Photo } from '@/photo';
|
||||
import PhotoRecipe from '@/recipe/PhotoRecipe';
|
||||
import PhotoTag from '@/tag/PhotoTag';
|
||||
import clsx from 'clsx/lite';
|
||||
import { formatDistanceToNowStrict } from 'date-fns';
|
||||
import AdminAboutMenu from './AdminAboutMenu';
|
||||
import PhotoLarge from '@/photo/PhotoLarge';
|
||||
import { ReactNode, useMemo } from 'react';
|
||||
import { Camera } from '@/camera';
|
||||
import { Lens } from '@/lens';
|
||||
import { Album } from '@/album';
|
||||
import { ReactNode } from 'react';
|
||||
import { useAppText } from '@/i18n/state/client';
|
||||
import PhotoAvatar from '@/photo/PhotoAvatar';
|
||||
import Link from 'next/link';
|
||||
import { PATH_ADMIN_ABOUT_EDIT } from '@/app/path';
|
||||
import { LuCirclePlus, LuUser } from 'react-icons/lu';
|
||||
import { LuCirclePlus } from 'react-icons/lu';
|
||||
import AdminEmptyState from '@/admin/AdminEmptyState';
|
||||
import { Place } from '@/place';
|
||||
import PlaceEntity from '@/place/PlaceEntity';
|
||||
import AdminAboutMenu from './AdminAboutMenu';
|
||||
import clsx from 'clsx/lite';
|
||||
|
||||
export default function AboutPageClient({
|
||||
title,
|
||||
subhead,
|
||||
descriptionHtml,
|
||||
photosCount = 0,
|
||||
photosOldest,
|
||||
photoAvatar,
|
||||
photoHero,
|
||||
camera,
|
||||
lens,
|
||||
recipe,
|
||||
film,
|
||||
tag,
|
||||
place,
|
||||
album,
|
||||
lastUpdated,
|
||||
}: {
|
||||
title?: string
|
||||
subhead?: string
|
||||
descriptionHtml?: ReactNode
|
||||
photosCount?: number
|
||||
photosOldest?: string
|
||||
photoAvatar?: Photo
|
||||
photoHero?: Photo
|
||||
camera?: Camera
|
||||
lens?: Lens
|
||||
recipe?: string
|
||||
film?: string
|
||||
tag?: string
|
||||
place?: Place
|
||||
album?: Album
|
||||
lastUpdated?: Date
|
||||
}) {
|
||||
const {
|
||||
isUserSignedIn,
|
||||
} = useAppState();
|
||||
|
||||
const { isUserSignedIn } = useAppState();
|
||||
const appText = useAppText();
|
||||
|
||||
const renderItem = (label: string, content?: ReactNode) => (
|
||||
<div
|
||||
key={label}
|
||||
className="border-t border-medium pt-1 space-y-px"
|
||||
>
|
||||
<div className="text-[13px] uppercase tracking-wide text-dim truncate">
|
||||
{label}
|
||||
</div>
|
||||
<div className="text-[16px] truncate">
|
||||
{content || '--'}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const items = useMemo(() => [
|
||||
renderItem(
|
||||
appText.about.photoCount,
|
||||
photosCount.toString().padStart(4, '0'),
|
||||
),
|
||||
renderItem(
|
||||
appText.about.firstPhoto,
|
||||
photosOldest?.slice(0, 10),
|
||||
),
|
||||
camera && renderItem(
|
||||
appText.about.topCamera,
|
||||
<PhotoCamera
|
||||
camera={camera}
|
||||
type="text-only"
|
||||
contrast="high"
|
||||
/>,
|
||||
),
|
||||
lens && renderItem(
|
||||
appText.about.topLens,
|
||||
<PhotoLens
|
||||
lens={lens}
|
||||
type="text-only"
|
||||
contrast="high"
|
||||
/>,
|
||||
),
|
||||
recipe && renderItem(
|
||||
appText.about.topRecipe,
|
||||
<PhotoRecipe
|
||||
recipe={recipe}
|
||||
type="text-only"
|
||||
contrast="high"
|
||||
/>,
|
||||
),
|
||||
film && renderItem(
|
||||
appText.about.topFilm,
|
||||
<PhotoFilm
|
||||
film={film}
|
||||
type="text-only"
|
||||
contrast="high"
|
||||
badged={false}
|
||||
/>,
|
||||
),
|
||||
tag && renderItem(
|
||||
appText.about.popularTag,
|
||||
<PhotoTag
|
||||
tag={tag}
|
||||
type="text-only"
|
||||
contrast="high"
|
||||
/>,
|
||||
),
|
||||
place && renderItem(
|
||||
appText.about.popularPlace,
|
||||
<PlaceEntity
|
||||
place={place}
|
||||
type="text-only"
|
||||
contrast="high"
|
||||
badged={false}
|
||||
/>,
|
||||
),
|
||||
album && renderItem(
|
||||
appText.about.recentAlbum,
|
||||
<PhotoAlbum
|
||||
album={album}
|
||||
type="text-only"
|
||||
contrast="high"
|
||||
/>,
|
||||
),
|
||||
].filter(Boolean), [
|
||||
appText.about,
|
||||
photosCount,
|
||||
photosOldest,
|
||||
camera,
|
||||
lens,
|
||||
recipe,
|
||||
film,
|
||||
album,
|
||||
place,
|
||||
tag,
|
||||
]);
|
||||
|
||||
return (
|
||||
<AnimateItems
|
||||
type="bottom"
|
||||
items={[<div
|
||||
key="about-page"
|
||||
className="space-y-12 mt-5"
|
||||
className="mt-5 max-w-2xl"
|
||||
>
|
||||
<AppGrid
|
||||
contentMain={<div className="space-y-8">
|
||||
<div className="flex items-center gap-4 sm:gap-6">
|
||||
<PhotoAvatar
|
||||
photo={photoAvatar}
|
||||
placeholder={<LuUser size={22} className="text-dim" />}
|
||||
/>
|
||||
<div
|
||||
className={clsx('sm:flex items-center justify-between grow')}
|
||||
>
|
||||
<div>
|
||||
<div className="font-bold">
|
||||
{title || appText.about.titleDefault}
|
||||
</div>
|
||||
{subhead &&
|
||||
<div>{subhead}</div>}
|
||||
</div>
|
||||
{lastUpdated && <div className={clsx('text-dim')}>
|
||||
{appText.about.updated(
|
||||
formatDistanceToNowStrict(lastUpdated),
|
||||
)}
|
||||
</div>}
|
||||
contentMain={<div className="space-y-6">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-2xl font-bold">
|
||||
{title || appText.about.titleDefault}
|
||||
</h1>
|
||||
{subhead &&
|
||||
<p className="text-medium">{subhead}</p>}
|
||||
</div>
|
||||
{isUserSignedIn && <AdminAboutMenu />}
|
||||
</div>
|
||||
@ -207,21 +58,10 @@ export default function AboutPageClient({
|
||||
includeContainer={false}
|
||||
className="gap-3! p-6!"
|
||||
>
|
||||
Add optional description
|
||||
Add description
|
||||
</AdminEmptyState>
|
||||
</Link>}
|
||||
<AnimateItems
|
||||
className={clsx(
|
||||
'grid gap-x-2 gap-y-6 grid-cols-2',
|
||||
items.length === 7 || items.length === 8
|
||||
? 'lg:grid-cols-4'
|
||||
: 'lg:grid-cols-3',
|
||||
)}
|
||||
items={items}
|
||||
/>
|
||||
</div>} />
|
||||
{photoHero &&
|
||||
<PhotoLarge photo={photoHero} />}
|
||||
</div>]}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -8,26 +8,13 @@ import FieldsetWithStatus from '@/components/FieldsetWithStatus';
|
||||
import AdminChildPage from '@/components/AdminChildPage';
|
||||
import { updateAboutAction } from './actions';
|
||||
import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus';
|
||||
import { Photo } from '@/photo';
|
||||
import { useAppText } from '@/i18n/state/client';
|
||||
import FieldsetPhotoChooser from '@/photo/form/FieldsetPhotoChooser';
|
||||
import { ABOUT_DESCRIPTION_DEFAULT } from '@/app/config';
|
||||
import { ABOUT_DESCRIPTION_DEFAULT, ABOUT_SUBHEAD, ABOUT_TITLE } from '@/app/config';
|
||||
|
||||
export default function AdminAboutEditPage({
|
||||
about,
|
||||
photoAvatar,
|
||||
photoHero,
|
||||
photos,
|
||||
photosCount,
|
||||
photosFavs,
|
||||
}: {
|
||||
about?: About
|
||||
photoAvatar?: Photo
|
||||
photoHero?: Photo
|
||||
photos: Photo[]
|
||||
photosCount: number
|
||||
photosFavs: Photo[]
|
||||
shouldResizeImages?: boolean
|
||||
}) {
|
||||
const appText = useAppText();
|
||||
|
||||
@ -40,32 +27,21 @@ export default function AdminAboutEditPage({
|
||||
breadcrumb="Edit About Page"
|
||||
>
|
||||
<form
|
||||
className="space-y-12 mt-12"
|
||||
className="space-y-8 mt-12 max-w-xl"
|
||||
action={updateAboutAction}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<FieldsetPhotoChooser
|
||||
id="photoIdAvatar"
|
||||
label="Avatar"
|
||||
value={aboutForm?.photoIdAvatar ?? photoAvatar?.id ?? ''}
|
||||
onChange={photoIdAvatar => setAboutForm(form =>
|
||||
({ ...form, photoIdAvatar }))}
|
||||
photo={photoAvatar}
|
||||
photos={photos}
|
||||
photosCount={photosCount}
|
||||
photosFavs={photosFavs}
|
||||
/>
|
||||
<FieldsetWithStatus
|
||||
label="Title"
|
||||
value={aboutForm?.title ?? ''}
|
||||
placeholder={appText.about.titleDefault}
|
||||
placeholder={ABOUT_TITLE || appText.about.titleDefault}
|
||||
onChange={title => setAboutForm(form =>
|
||||
({ ...form, title }))}
|
||||
/>
|
||||
<FieldsetWithStatus
|
||||
label="Subhead"
|
||||
type={!aboutForm?.title ? 'hidden' : undefined}
|
||||
value={aboutForm?.subhead ?? ''}
|
||||
placeholder={ABOUT_SUBHEAD}
|
||||
onChange={subhead => setAboutForm(form =>
|
||||
({ ...form, subhead }))}
|
||||
/>
|
||||
@ -77,17 +53,9 @@ export default function AdminAboutEditPage({
|
||||
onChange={description => setAboutForm(form =>
|
||||
({ ...form, description }))}
|
||||
/>
|
||||
<FieldsetPhotoChooser
|
||||
id="photoIdHero"
|
||||
label="Hero"
|
||||
value={aboutForm?.photoIdHero || photoHero?.id || ''}
|
||||
onChange={photoIdHero => setAboutForm(form =>
|
||||
({ ...form, photoIdHero }))}
|
||||
photo={photoHero}
|
||||
photos={photos}
|
||||
photosCount={photosCount}
|
||||
photosFavs={photosFavs}
|
||||
/>
|
||||
<p className="text-dim text-sm">
|
||||
Supports simple formatting: <b>, <i>, <u>, <br>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<LinkWithStatus
|
||||
|
||||
@ -1,37 +1,10 @@
|
||||
import { getPhotoCached, getPhotosCached } from '@/photo/cache';
|
||||
import { About } from '.';
|
||||
import { TAG_FAVS } from '@/tag';
|
||||
import { getAbout } from './query';
|
||||
import { getAboutCached } from './cache';
|
||||
|
||||
const getAboutAvatar = (about?: About) =>
|
||||
about?.photoIdAvatar
|
||||
? getPhotoCached(about?.photoIdAvatar ?? '', true)
|
||||
: undefined;
|
||||
|
||||
const getAboutHero = (about?: About) =>
|
||||
about?.photoIdHero
|
||||
? getPhotoCached(about?.photoIdHero ?? '', true)
|
||||
// Fall back to favorite photos if no hero photo is set
|
||||
: getPhotosCached({ tag: TAG_FAVS, limit: 1 })
|
||||
.then(photos => photos.length > 0
|
||||
? photos[0]
|
||||
// Fall back to oldest photo if no favorite photos exist
|
||||
: getPhotosCached({ limit: 1, sortBy: 'takenAtAsc' })
|
||||
.then(photos => photos[0]));
|
||||
|
||||
export const getAboutData = () =>
|
||||
getAbout()
|
||||
.then(async about => ({
|
||||
about,
|
||||
photoAvatar: await getAboutAvatar(about),
|
||||
photoHero: await getAboutHero(about),
|
||||
}));
|
||||
.then(about => ({ about }));
|
||||
|
||||
export const getAboutDataCached = () =>
|
||||
getAboutCached()
|
||||
.then(async about => ({
|
||||
about,
|
||||
photoAvatar: await getAboutAvatar(about),
|
||||
photoHero: await getAboutHero(about),
|
||||
}));
|
||||
.then(about => ({ about }));
|
||||
|
||||
@ -153,9 +153,17 @@ export const SIDEBAR_TEXT =
|
||||
process.env.NEXT_PUBLIC_PAGE_ABOUT ||
|
||||
process.env.NEXT_PUBLIC_SITE_ABOUT;
|
||||
|
||||
export const ABOUT_TITLE =
|
||||
process.env.NEXT_PUBLIC_ABOUT_TITLE ||
|
||||
NAV_TITLE;
|
||||
|
||||
export const ABOUT_SUBHEAD =
|
||||
process.env.NEXT_PUBLIC_ABOUT_SUBHEAD;
|
||||
|
||||
export const ABOUT_DESCRIPTION_DEFAULT =
|
||||
process.env.NEXT_PUBLIC_META_DESCRIPTION ||
|
||||
process.env.NEXT_PUBLIC_SIDEBAR_TEXT;
|
||||
process.env.NEXT_PUBLIC_ABOUT_DESCRIPTION ||
|
||||
process.env.NEXT_PUBLIC_SIDEBAR_TEXT ||
|
||||
process.env.NEXT_PUBLIC_META_DESCRIPTION;
|
||||
|
||||
// STORAGE
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user