diff --git a/README.md b/README.md index 22dcebf2..0c5b02c6 100644 --- a/README.md +++ b/README.md @@ -146,14 +146,14 @@ Application behavior can be changed by configuring the following environment var #### Sorting - `NEXT_PUBLIC_DEFAULT_SORT` - - Sets default sort on grid/feed homepages + - Sets default sort on grid/full homepages - Accepted values: - `taken-at` (default) - `taken-at-oldest-first` - `uploaded-at` - `uploaded-at-oldest-first` - `NEXT_PUBLIC_PRIORITY_BASED_SORTING = 1` takes priority field into account when sorting photos (⚠️ enabling may have performance consequences) -- `NEXT_PUBLIC_SHOW_SORT_CONTROL = 1` shows sort control in desktop nav on grid/feed homepages +- `NEXT_PUBLIC_SHOW_SORT_CONTROL = 1` shows sort control in desktop nav on grid/full homepages #### Display - `NEXT_PUBLIC_HIDE_KEYBOARD_SHORTCUT_TOOLTIPS = 1` hides keyboard shortcut hints in areas like the main nav, and previous/next photo links @@ -317,8 +317,8 @@ Thank you ❤️ translators: [@sconetto](https://github.com/sconetto) (`pt-br`, #### Why are my grid thumbnails so small? > Thumbnail grid density (seen on `/grid`, tag overviews, and other photo sets) is dependent on aspect ratio configuration (ratios of 1 or less have more photos per row). This can be overridden by setting `NEXT_PUBLIC_SHOW_LARGE_THUMBNAILS = 1`. -#### How secure are photos marked “hidden?” -> While all hidden paths (`/tag/hidden/*`) require authentication, raw links to individual photo assets remain publicly accessible. Randomly generated urls from storage providers are only secure via obscurity. Use with caution. +#### How secure are photos marked “private?” +> While all private paths (`/tag/private/*`) require authentication, raw links to individual photo assets remain publicly accessible. Randomly generated urls from storage providers are only secure via obscurity. Use with caution. #### My images/content have fallen out of sync with my database and/or my production site no longer matches local development. What do I do? > Navigate to `/admin/configuration` and click "Clear Cache." diff --git a/__tests__/path.test.ts b/__tests__/path.test.ts index 9c8f8c56..e336ad22 100644 --- a/__tests__/path.test.ts +++ b/__tests__/path.test.ts @@ -12,7 +12,7 @@ import { isPathTag, isPathTagPhoto, } from '@/app/paths'; -import { TAG_HIDDEN } from '@/tag'; +import { TAG_PRIVATE } from '@/tag'; const PHOTO_ID = 'UsKSGcbt'; const TAG = 'tag-name'; @@ -25,7 +25,7 @@ const FOCAL_LENGTH_STRING = `${FOCAL_LENGTH}mm`; const PATH_ROOT = '/'; const PATH_GRID = '/grid'; -const PATH_FEED = '/feed'; +const PATH_FULL = '/full'; const PATH_ADMIN = '/admin/photos'; const PATH_OG = '/og'; const PATH_OG_ALL = `${PATH_OG}/all`; @@ -36,8 +36,8 @@ const PATH_PHOTO = `/p/${PHOTO_ID}`; const PATH_TAG = `/tag/${TAG}`; const PATH_TAG_PHOTO = `${PATH_TAG}/${PHOTO_ID}`; -const PATH_TAG_HIDDEN = `/tag/${TAG_HIDDEN}`; -const PATH_TAG_HIDDEN_PHOTO = `${PATH_TAG_HIDDEN}/${PHOTO_ID}`; +const PATH_TAG_PRIVATE = `/tag/${TAG_PRIVATE}`; +const PATH_TAG_PRIVATE_PHOTO = `${PATH_TAG_PRIVATE}/${PHOTO_ID}`; const PATH_CAMERA = `/shot-on/${CAMERA_MAKE}/${CAMERA_MODEL}`; const PATH_CAMERA_PHOTO = `${PATH_CAMERA}/${PHOTO_ID}`; @@ -62,8 +62,8 @@ describe('Paths', () => { expect(isPathProtected(PATH_OG)).toBe(true); expect(isPathProtected(PATH_OG_ALL)).toBe(true); expect(isPathProtected(PATH_OG_SAMPLE)).toBe(true); - expect(isPathProtected(PATH_TAG_HIDDEN)).toBe(true); - expect(isPathProtected(PATH_TAG_HIDDEN_PHOTO)).toBe(true); + expect(isPathProtected(PATH_TAG_PRIVATE)).toBe(true); + expect(isPathProtected(PATH_TAG_PRIVATE_PHOTO)).toBe(true); }); it('can be classified', () => { // Positive @@ -123,7 +123,7 @@ describe('Paths', () => { // Root expect(getEscapePath(PATH_ROOT)).toEqual(undefined); expect(getEscapePath(PATH_GRID)).toEqual(undefined); - expect(getEscapePath(PATH_FEED)).toEqual(undefined); + expect(getEscapePath(PATH_FULL)).toEqual(undefined); expect(getEscapePath(PATH_ADMIN)).toEqual(undefined); // Photo expect(getEscapePath(PATH_PHOTO)).toEqual(PATH_ROOT); diff --git a/app/feed.json/route.ts b/app/feed.json/route.ts index 0cd6fe67..07ded6a4 100644 --- a/app/feed.json/route.ts +++ b/app/feed.json/route.ts @@ -1,17 +1,15 @@ import { getPhotosCached } from '@/photo/cache'; import { SITE_FEEDS_ENABLED } from '@/app/config'; -import { FEED_PHOTO_REQUEST_LIMIT } from '@/feed'; import { formatFeedJson } from '@/feed/json'; +import { PROGRAMMATIC_QUERY_OPTIONS } from '@/feed'; // Cache for 24 hours export const revalidate = 86_400; export async function GET() { if (SITE_FEEDS_ENABLED) { - const photos = await getPhotosCached({ - limit: FEED_PHOTO_REQUEST_LIMIT, - sortBy: 'createdAt', - }).catch(() => []); + const photos = await getPhotosCached(PROGRAMMATIC_QUERY_OPTIONS) + .catch(() => []); return Response.json(formatFeedJson(photos)); } else { return new Response('Feeds disabled', { status: 404 }); diff --git a/app/feed/[sortType]/[sortOrder]/page.tsx b/app/full/[sortType]/[sortOrder]/page.tsx similarity index 55% rename from app/feed/[sortType]/[sortOrder]/page.tsx rename to app/full/[sortType]/[sortOrder]/page.tsx index bb217755..1f74f185 100644 --- a/app/feed/[sortType]/[sortOrder]/page.tsx +++ b/app/full/[sortType]/[sortOrder]/page.tsx @@ -1,52 +1,51 @@ -import { - INFINITE_SCROLL_FEED_INITIAL, - generateOgImageMetaForPhotos, -} from '@/photo'; +import { generateOgImageMetaForPhotos } from '@/photo'; import PhotosEmptyState from '@/photo/PhotosEmptyState'; import { Metadata } from 'next/types'; import { cache } from 'react'; import { getPhotos } from '@/photo/db/query'; -import PhotoFeedPage from '@/photo/PhotoFeedPage'; +import PhotoFullPage from '@/photo/PhotoFullPage'; import { getPhotosMetaCached } from '@/photo/cache'; import { SortProps } from '@/photo/db/sort'; import { getSortOptionsFromParams } from '@/photo/db/sort-path'; import { PhotoQueryOptions } from '@/photo/db'; +import { FEED_META_QUERY_OPTIONS, getFeedQueryOptions } from '@/feed'; export const maxDuration = 60; -const getPhotosCached = cache((options: PhotoQueryOptions) => getPhotos({ - ...options, - limit: INFINITE_SCROLL_FEED_INITIAL, -})); +const getPhotosCached = cache((options: PhotoQueryOptions) => + getPhotos(getFeedQueryOptions({ + isGrid: false, + ...options, + }))); export async function generateMetadata({ params, }: SortProps): Promise { - const options = await getSortOptionsFromParams(params); - const photos = await getPhotosCached(options) + const sortOptions = await getSortOptionsFromParams(params); + const photos = await getPhotosCached(sortOptions) .catch(() => []); return generateOgImageMetaForPhotos(photos); } -export default async function FeedPageSort({ params }: SortProps) { - const options = await getSortOptionsFromParams(params); +export default async function FullPageSort({ params }: SortProps) { + const sortOptions = await getSortOptionsFromParams(params); const [ photos, photosCount, ] = await Promise.all([ - getPhotosCached(options) + getPhotosCached(sortOptions) .catch(() => []), - getPhotosMetaCached(options) + getPhotosMetaCached(FEED_META_QUERY_OPTIONS) .then(({ count }) => count) .catch(() => 0), ]); return ( photos.length > 0 - ? : ); diff --git a/app/feed/page.tsx b/app/full/page.tsx similarity index 58% rename from app/feed/page.tsx rename to app/full/page.tsx index 7034e0b5..57e41bb2 100644 --- a/app/feed/page.tsx +++ b/app/full/page.tsx @@ -1,45 +1,41 @@ -import { - INFINITE_SCROLL_FEED_INITIAL, - generateOgImageMetaForPhotos, -} from '@/photo'; +import { generateOgImageMetaForPhotos } from '@/photo'; import PhotosEmptyState from '@/photo/PhotosEmptyState'; import { Metadata } from 'next/types'; import { cache } from 'react'; import { getPhotos } from '@/photo/db/query'; -import PhotoFeedPage from '@/photo/PhotoFeedPage'; +import PhotoFullPage from '@/photo/PhotoFullPage'; import { getPhotosMetaCached } from '@/photo/cache'; import { USER_DEFAULT_SORT_OPTIONS } from '@/app/config'; -import { PhotoQueryOptions } from '@/photo/db'; +import { FEED_META_QUERY_OPTIONS, getFeedQueryOptions } from '@/feed'; export const dynamic = 'force-static'; export const maxDuration = 60; -const getPhotosCached = cache((options: PhotoQueryOptions) => getPhotos({ - ...options, - limit: INFINITE_SCROLL_FEED_INITIAL, -})); +const getPhotosCached = cache(() => getPhotos(getFeedQueryOptions({ + isGrid: false, +}))); export async function generateMetadata(): Promise { - const photos = await getPhotosCached(USER_DEFAULT_SORT_OPTIONS) + const photos = await getPhotosCached() .catch(() => []); return generateOgImageMetaForPhotos(photos); } -export default async function FeedPage() { +export default async function FullPage() { const [ photos, photosCount, ] = await Promise.all([ - getPhotosCached(USER_DEFAULT_SORT_OPTIONS) + getPhotosCached() .catch(() => []), - getPhotosMetaCached(USER_DEFAULT_SORT_OPTIONS) + getPhotosMetaCached(FEED_META_QUERY_OPTIONS) .then(({ count }) => count) .catch(() => 0), ]); return ( photos.length > 0 - ? getPhotos({ - ...options, - limit: INFINITE_SCROLL_GRID_INITIAL, -})); +const getPhotosCached = cache((options: PhotoQueryOptions) => + getPhotos(getFeedQueryOptions({ + isGrid: true, + ...options, + }))); export async function generateMetadata({ params, }: SortProps): Promise { - const options = await getSortOptionsFromParams(params); - const photos = await getPhotosCached(options) + const sortOptions = await getSortOptionsFromParams(params); + const photos = await getPhotosCached(sortOptions) .catch(() => []); return generateOgImageMetaForPhotos(photos); } export default async function GridPage({ params }: SortProps) { - const options = await getSortOptionsFromParams(params); + const sortOptions = await getSortOptionsFromParams(params); const [ photos, photosCount, categories, ] = await Promise.all([ - getPhotosCached(options) + getPhotosCached(sortOptions) .catch(() => []), - getPhotosMetaCached(options) + getPhotosMetaCached(FEED_META_QUERY_OPTIONS) .then(({ count }) => count) .catch(() => 0), getDataForCategoriesCached(), @@ -50,7 +49,7 @@ export default async function GridPage({ params }: SortProps) { {...{ photos, photosCount, - ...options, + ...sortOptions, ...categories, }} /> diff --git a/app/grid/page.tsx b/app/grid/page.tsx index bce9cd24..331e39ef 100644 --- a/app/grid/page.tsx +++ b/app/grid/page.tsx @@ -1,7 +1,4 @@ -import { - INFINITE_SCROLL_GRID_INITIAL, - generateOgImageMetaForPhotos, -} from '@/photo'; +import { generateOgImageMetaForPhotos } from '@/photo'; import PhotosEmptyState from '@/photo/PhotosEmptyState'; import { Metadata } from 'next/types'; import { getPhotos } from '@/photo/db/query'; @@ -10,18 +7,17 @@ import PhotoGridPage from '@/photo/PhotoGridPage'; import { getDataForCategoriesCached } from '@/category/cache'; import { getPhotosMetaCached } from '@/photo/cache'; import { USER_DEFAULT_SORT_OPTIONS } from '@/app/config'; -import { PhotoQueryOptions } from '@/photo/db'; +import { FEED_META_QUERY_OPTIONS, getFeedQueryOptions } from '@/feed'; export const dynamic = 'force-static'; export const maxDuration = 60; -const getPhotosCached = cache((options: PhotoQueryOptions) => getPhotos({ - ...options, - limit: INFINITE_SCROLL_GRID_INITIAL, -})); +const getPhotosCached = cache(() => getPhotos(getFeedQueryOptions({ + isGrid: true, +}))); export async function generateMetadata(): Promise { - const photos = await getPhotosCached(USER_DEFAULT_SORT_OPTIONS) + const photos = await getPhotosCached() .catch(() => []); return generateOgImageMetaForPhotos(photos); } @@ -32,9 +28,9 @@ export default async function GridPage() { photosCount, categories, ] = await Promise.all([ - getPhotosCached(USER_DEFAULT_SORT_OPTIONS) + getPhotosCached() .catch(() => []), - getPhotosMetaCached(USER_DEFAULT_SORT_OPTIONS) + getPhotosMetaCached(FEED_META_QUERY_OPTIONS) .then(({ count }) => count) .catch(() => 0), getDataForCategoriesCached(), diff --git a/app/layout.tsx b/app/layout.tsx index 6bf350a4..62b8a0cd 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -30,6 +30,7 @@ import RecipeModal from '@/recipe/RecipeModal'; import ThemeColors from '@/app/ThemeColors'; import AppTextProvider from '@/i18n/state/AppTextProvider'; import SharedHoverProvider from '@/components/shared-hover/SharedHoverProvider'; +import { PATH_FEED_JSON, PATH_RSS_XML } from '@/app/paths'; import '../tailwind.css'; @@ -71,7 +72,8 @@ export const metadata: Metadata = { ...SITE_FEEDS_ENABLED && { alternates: { types: { - 'application/rss+xml': '/rss.xml', + 'application/rss+xml': PATH_RSS_XML, + 'application/json': PATH_FEED_JSON, }, }, }, diff --git a/app/p/[photoId]/page.tsx b/app/p/[photoId]/page.tsx index 8ac6b4a6..33a7a29a 100644 --- a/app/p/[photoId]/page.tsx +++ b/app/p/[photoId]/page.tsx @@ -18,7 +18,13 @@ import { staticallyGeneratePhotosIfConfigured } from '@/app/static'; export const maxDuration = 60; const getPhotosNearIdCachedCached = cache((photoId: string) => - getPhotosNearIdCached(photoId, { limit: RELATED_GRID_PHOTOS_TO_SHOW + 2 })); + getPhotosNearIdCached( + photoId, { + limit: RELATED_GRID_PHOTOS_TO_SHOW + 2, + }, + // Don't show photo in context when excluded from feeds + true, + )); export const generateStaticParams = staticallyGeneratePhotosIfConfigured( 'page', diff --git a/app/page.tsx b/app/page.tsx index f6a6b435..77cb5f6d 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,32 +1,25 @@ -import { - INFINITE_SCROLL_FEED_INITIAL, - INFINITE_SCROLL_GRID_INITIAL, - generateOgImageMetaForPhotos, -} from '@/photo'; +import { generateOgImageMetaForPhotos } from '@/photo'; import PhotosEmptyState from '@/photo/PhotosEmptyState'; import { Metadata } from 'next/types'; import { cache } from 'react'; import { getPhotos } from '@/photo/db/query'; import { GRID_HOMEPAGE_ENABLED, USER_DEFAULT_SORT_OPTIONS } from '@/app/config'; import { NULL_CATEGORY_DATA } from '@/category/data'; -import PhotoFeedPage from '@/photo/PhotoFeedPage'; +import PhotoFullPage from '@/photo/PhotoFullPage'; import PhotoGridPage from '@/photo/PhotoGridPage'; import { getDataForCategoriesCached } from '@/category/cache'; import { getPhotosMetaCached } from '@/photo/cache'; -import { PhotoQueryOptions } from '@/photo/db'; +import { FEED_META_QUERY_OPTIONS, getFeedQueryOptions } from '@/feed'; export const dynamic = 'force-static'; export const maxDuration = 60; -const getPhotosCached = cache((options: PhotoQueryOptions) => getPhotos({ - ...options, - limit: GRID_HOMEPAGE_ENABLED - ? INFINITE_SCROLL_GRID_INITIAL - : INFINITE_SCROLL_FEED_INITIAL, -})); +const getPhotosCached = cache(() => getPhotos(getFeedQueryOptions({ + isGrid: GRID_HOMEPAGE_ENABLED, +}))); export async function generateMetadata(): Promise { - const photos = await getPhotosCached(USER_DEFAULT_SORT_OPTIONS) + const photos = await getPhotosCached() .catch(() => []); return generateOgImageMetaForPhotos(photos); } @@ -37,9 +30,9 @@ export default async function HomePage() { photosCount, categories, ] = await Promise.all([ - getPhotosCached(USER_DEFAULT_SORT_OPTIONS) + getPhotosCached() .catch(() => []), - getPhotosMetaCached(USER_DEFAULT_SORT_OPTIONS) + getPhotosMetaCached(FEED_META_QUERY_OPTIONS) .then(({ count }) => count) .catch(() => 0), GRID_HOMEPAGE_ENABLED @@ -58,7 +51,7 @@ export default async function HomePage() { ...categories, }} /> - : []); + const photos = await getPhotosCached(PROGRAMMATIC_QUERY_OPTIONS) + .catch(() => []); return new Response( formatFeedRssXml(photos), { headers: { 'Content-Type': 'text/xml' } }, diff --git a/app/sitemap.ts b/app/sitemap.ts index 117dce63..1ed45743 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -1,6 +1,8 @@ import type { MetadataRoute } from 'next'; import { getDataForCategoriesCached } from '@/category/cache'; import { + ABSOLUTE_PATH_FULL, + ABSOLUTE_PATH_GRID, absolutePathForCamera, absolutePathForFilm, absolutePathForFocalLength, @@ -72,9 +74,9 @@ export default async function sitemap(): Promise { priority: PRIORITY_HOME, lastModified: lastModifiedSite, }, - // Grid or Feed + // Grid or full { - url: GRID_HOMEPAGE_ENABLED ? `${BASE_URL}/feed` : `${BASE_URL}/grid`, + url: GRID_HOMEPAGE_ENABLED ? ABSOLUTE_PATH_FULL : ABSOLUTE_PATH_GRID, priority: PRIORITY_HOME_VIEW, lastModified: lastModifiedSite, }, diff --git a/app/tag/hidden/[photoId]/page.tsx b/app/tag/private/[photoId]/page.tsx similarity index 90% rename from app/tag/hidden/[photoId]/page.tsx rename to app/tag/private/[photoId]/page.tsx index 827379a2..9bf59fcd 100644 --- a/app/tag/hidden/[photoId]/page.tsx +++ b/app/tag/private/[photoId]/page.tsx @@ -9,7 +9,7 @@ import { getPhotosNearIdCached, } from '@/photo/cache'; import { PATH_ROOT, absolutePathForPhoto } from '@/app/paths'; -import { TAG_HIDDEN } from '@/tag'; +import { TAG_PRIVATE } from '@/tag'; import { Metadata } from 'next'; import { redirect } from 'next/navigation'; import { cache } from 'react'; @@ -36,7 +36,7 @@ export async function generateMetadata({ const title = titleForPhoto(photo); const description = descriptionForPhoto(photo); const descriptionHtml = descriptionForPhoto(photo, true); - const url = absolutePathForPhoto({ photo, tag: TAG_HIDDEN }); + const url = absolutePathForPhoto({ photo, tag: TAG_PRIVATE }); return { title, @@ -54,7 +54,7 @@ export async function generateMetadata({ }; } -export default async function PhotoTagHiddenPage({ +export default async function PhotoTagPrivatePage({ params, }: PhotoTagProps) { const { photoId } = await params; @@ -74,7 +74,7 @@ export default async function PhotoTagHiddenPage({ indexNumber, count, dateRange, - tag: TAG_HIDDEN, + tag: TAG_PRIVATE, shouldShare: false, includeFavoriteInAdminMenu: false, }} /> diff --git a/app/tag/hidden/page.tsx b/app/tag/private/page.tsx similarity index 78% rename from app/tag/hidden/page.tsx rename to app/tag/private/page.tsx index 405cbaf8..8c078006 100644 --- a/app/tag/hidden/page.tsx +++ b/app/tag/private/page.tsx @@ -4,8 +4,8 @@ import AppGrid from '@/components/AppGrid'; import PhotoGrid from '@/photo/PhotoGrid'; import { getPhotosMetaCached, getPhotosNoStore } from '@/photo/cache'; import { absolutePathForTag } from '@/app/paths'; -import { TAG_HIDDEN, descriptionForTaggedPhotos, titleForTag } from '@/tag'; -import HiddenHeader from '@/tag/HiddenHeader'; +import { TAG_PRIVATE, descriptionForTaggedPhotos, titleForTag } from '@/tag'; +import PrivateHeader from '@/tag/PrivateHeader'; import { Metadata } from 'next'; import { cache } from 'react'; import { getAppText } from '@/i18n/state/server'; @@ -20,7 +20,7 @@ export async function generateMetadata(): Promise { const appText = await getAppText(); - const title = titleForTag(TAG_HIDDEN, undefined, appText, count); + const title = titleForTag(TAG_PRIVATE, undefined, appText, count); const description = descriptionForTaggedPhotos( undefined, @@ -29,7 +29,7 @@ export async function generateMetadata(): Promise { count, dateRange, ); - const url = absolutePathForTag(TAG_HIDDEN); + const url = absolutePathForTag(TAG_PRIVATE); return { title, @@ -46,7 +46,7 @@ export async function generateMetadata(): Promise { }; } -export default async function HiddenTagPage() { +export default async function PrivateTagPage() { const [ photos, { count, dateRange }, @@ -60,15 +60,15 @@ export default async function HiddenTagPage() { contentMain={
]} animateOnFirstLoadOnly />
- Only visible to authenticated admins + Visible only to admins (uploads only secure via obscurity)
diff --git a/middleware.ts b/middleware.ts index ba0e2bad..56ff71ae 100644 --- a/middleware.ts +++ b/middleware.ts @@ -46,12 +46,12 @@ export const config = { // - /_next/image* // - /favicon.ico + /favicons/* // - /grid - // - /feed + // - /full // - / (root) // - /home-image // - /template-image // - /template-image-tight // - /template-url // eslint-disable-next-line max-len - matcher: ['/((?!api$|api/auth|_next/static|_next/image|favicon.ico$|favicons/|grid$|feed$|home-image$|template-image$|template-image-tight$|template-url$|$).*)'], + matcher: ['/((?!api$|api/auth|_next/static|_next/image|favicon.ico$|favicons/|grid$|full$|home-image$|template-image$|template-image-tight$|template-url$|$).*)'], }; diff --git a/src/admin/AdminAppConfigurationClient.tsx b/src/admin/AdminAppConfigurationClient.tsx index c3b94208..4672aabb 100644 --- a/src/admin/AdminAppConfigurationClient.tsx +++ b/src/admin/AdminAppConfigurationClient.tsx @@ -635,7 +635,7 @@ export default function AdminAppConfigurationClient({ )} )}
- Change default sort on grid/feed homepages + Change default sort on grid/full homepages {renderEnvVars(['NEXT_PUBLIC_DEFAULT_SORT'])} Set environment variable to {'"1"'} to - show sort control in desktop nav on grid/feed homepages: + show sort control in desktop nav on grid/full homepages: {renderEnvVars(['NEXT_PUBLIC_SHOW_SORT_CONTROL'])} diff --git a/src/admin/AdminBatchUploadActions.tsx b/src/admin/AdminBatchUploadActions.tsx index 2e3c0ea8..f5576e26 100644 --- a/src/admin/AdminBatchUploadActions.tsx +++ b/src/admin/AdminBatchUploadActions.tsx @@ -13,7 +13,7 @@ import { import sleep from '@/utility/sleep'; import { readStreamableValue } from 'ai/rsc'; import { useRouter } from 'next/navigation'; -import { Dispatch, SetStateAction, useRef, useState } from 'react'; +import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react'; import { BiCheckCircle } from 'react-icons/bi'; import ProgressButton from '@/components/primitives/ProgressButton'; import { UrlAddStatus } from './AdminUploadsClient'; @@ -22,8 +22,9 @@ import DeleteUploadButton from './DeleteUploadButton'; import { useAppState } from '@/app/AppState'; import { pluralize } from '@/utility/string'; import FieldsetFavs from '@/photo/form/FieldsetFavs'; -import FieldsetHidden from '@/photo/form/FieldsetHidden'; +import FieldsetPrivate from '@/photo/form/FieldsetPrivate'; import IconAddUpload from '@/components/icons/IconAddUpload'; +import FieldsetExclude from '@/photo/form/FieldsetExclude'; const UPLOAD_BATCH_SIZE = 2; @@ -53,6 +54,7 @@ export default function AdminBatchUploadActions({ const [showBulkSettings, setShowBulkSettings] = useState(false); const [tags, setTags] = useState(''); const [favorite, setFavorite] = useState('false'); + const [excludeFromFeeds, setExcludeFromFeeds] = useState('false'); const [hidden, setHidden] = useState('false'); const [tagErrorMessage, setTagErrorMessage] = useState(''); @@ -76,6 +78,7 @@ export default function AdminBatchUploadActions({ ...showBulkSettings && { tags, favorite, + excludeFromFeeds, hidden, }, takenAtLocal: generateLocalPostgresString(), @@ -123,12 +126,19 @@ export default function AdminBatchUploadActions({ } }; + useEffect(() => { + if (hidden === 'true') { + setFavorite('false'); + setExcludeFromFeeds('false'); + } + }, [hidden]); + return ( <> {actionErrorMessage && {actionErrorMessage}} - -
+ +
{showBulkSettings @@ -154,20 +164,25 @@ export default function AdminBatchUploadActions({ readOnly={isAdding} className="relative z-10" /> -
+
} -
+
[] = [{ label: appText.admin.edit, icon: , href: pathForAdminPhotoEdit(photo.id), ...showKeyCommands && { keyCommand: KEY_COMMANDS.edit }, @@ -94,19 +94,20 @@ export default function AdminPhotoMenu({ }); } items.push({ - label: photo.hidden ? appText.admin.unhide : appText.admin.hide, - icon: , - action: () => toggleHidePhotoAction( + action: () => togglePrivatePhotoAction( photo.id, - redirectPathOnHideToggle, + redirectPathOnPrivateToggle, ) .then(() => revalidatePhoto?.(photo.id)), ...showKeyCommands && { - keyCommand: KEY_COMMANDS.toggleHide, + keyCommand: KEY_COMMANDS.togglePrivate, }, }); items.push({ @@ -146,7 +147,7 @@ export default function AdminPhotoMenu({ includeFavorite, isFav, shouldRedirectFav, - redirectPathOnHideToggle, + redirectPathOnPrivateToggle, revalidatePhoto, ]); diff --git a/src/admin/AdminPhotosTable.tsx b/src/admin/AdminPhotosTable.tsx index 8ff5975e..39c7fc22 100644 --- a/src/admin/AdminPhotosTable.tsx +++ b/src/admin/AdminPhotosTable.tsx @@ -17,6 +17,7 @@ import { Timezone } from '@/utility/timezone'; import IconHidden from '@/components/icons/IconHidden'; import Tooltip from '@/components/Tooltip'; import { photoNeedsToBeSynced, getPhotoSyncStatusText } from '@/photo/sync'; +import IconLock from '@/components/icons/IconLock'; export default function AdminPhotosTable({ photos, @@ -76,13 +77,20 @@ export default function AdminPhotosTable({ {titleForPhoto(photo, false)} - {photo.hidden && + {photo.excludeFromFeeds && !photo.hidden && } + {photo.hidden && + + + } {photo.priorityOrder !== null && {isTagFavs(tag) - ? + ? : }
{count} diff --git a/src/admin/AdminUploadsTableRow.tsx b/src/admin/AdminUploadsTableRow.tsx index 1394ddf7..6cde66b3 100644 --- a/src/admin/AdminUploadsTableRow.tsx +++ b/src/admin/AdminUploadsTableRow.tsx @@ -75,7 +75,7 @@ export default function AdminUploadsTableRow({ className={clsx( 'flex items-center grow', 'transition-opacity', - 'rounded-md overflow-hidden', + 'rounded-lg overflow-hidden', 'border-medium bg-extra-dim', isAdding && !isComplete && status !== 'adding' && 'opacity-30', )} diff --git a/src/app/AppViewSwitcher.tsx b/src/app/AppViewSwitcher.tsx index 0476515f..20620d98 100644 --- a/src/app/AppViewSwitcher.tsx +++ b/src/app/AppViewSwitcher.tsx @@ -1,10 +1,10 @@ import Switcher from '@/components/Switcher'; import SwitcherItem from '@/components/SwitcherItem'; -import IconFeed from '@/components/icons/IconFeed'; +import IconFull from '@/components/icons/IconFull'; import IconGrid from '@/components/icons/IconGrid'; import { doesPathOfferSort, - PATH_FEED_INFERRED, + PATH_FULL_INFERRED, PATH_GRID_INFERRED, } from '@/app/paths'; import IconSearch from '../components/icons/IconSearch'; @@ -26,7 +26,7 @@ import IconSort from '@/components/icons/IconSort'; import { getSortConfigFromPath } from '@/photo/db/sort-path'; import { motion } from 'framer-motion'; -export type SwitcherSelection = 'feed' | 'grid' | 'admin'; +export type SwitcherSelection = 'full' | 'grid' | 'admin'; const GAP_CLASS = 'mr-1.5 sm:mr-2'; @@ -55,7 +55,7 @@ export default function AppViewSwitcher({ sortBy, isAscending, pathGrid, - pathFeed, + pathFull, pathSort, } = getSortConfigFromPath(pathname); @@ -68,14 +68,14 @@ export default function AppViewSwitcher({ hasLoadedRef.current = true; }, [invalidateSwr, sortBy]); - const refHrefFeed = useRef(null); + const refHrefFull = useRef(null); const refHrefGrid = useRef(null); const onKeyDown = useCallback((e: KeyboardEvent) => { if (!e.metaKey) { switch (e.key.toLocaleUpperCase()) { - case KEY_COMMANDS.feed: - if (pathname !== PATH_FEED_INFERRED) { refHrefFeed.current?.click(); } + case KEY_COMMANDS.full: + if (pathname !== PATH_FULL_INFERRED) { refHrefFull.current?.click(); } break; case KEY_COMMANDS.grid: if (pathname !== PATH_GRID_INFERRED) { refHrefGrid.current?.click(); } @@ -90,15 +90,15 @@ export default function AppViewSwitcher({ const [isAdminMenuOpen, setIsAdminMenuOpen] = useState(false); - const renderItemFeed = + const renderItemFull = } - href={pathFeed} - hrefRef={refHrefFeed} - active={currentSelection === 'feed'} + icon={} + href={pathFull} + hrefRef={refHrefFull} + active={currentSelection === 'full'} tooltip={{...SHOW_KEYBOARD_SHORTCUT_TOOLTIPS && { - content: appText.nav.feed, - keyCommand: KEY_COMMANDS.feed, + content: appText.nav.full, + keyCommand: KEY_COMMANDS.full, }}} noPadding />; @@ -119,8 +119,8 @@ export default function AppViewSwitcher({ return (
- {GRID_HOMEPAGE_ENABLED ? renderItemGrid : renderItemFeed} - {GRID_HOMEPAGE_ENABLED ? renderItemFeed : renderItemGrid} + {GRID_HOMEPAGE_ENABLED ? renderItemGrid : renderItemFull} + {GRID_HOMEPAGE_ENABLED ? renderItemFull : renderItemGrid} {/* Show spinner if admin is suspected to be logged in */} {(isUserSignedInEager && !isUserSignedIn) && { if (pathname === PATH_ROOT) { - return GRID_HOMEPAGE_ENABLED ? 'grid' : 'feed'; + return GRID_HOMEPAGE_ENABLED ? 'grid' : 'full'; } else if (isPathGrid(pathname)) { return 'grid'; - } else if (isPathFeed(pathname)) { - return 'feed'; + } else if (isPathFull(pathname)) { + return 'full'; } else if (isPathProtected(pathname)) { return 'admin'; } diff --git a/src/app/paths.ts b/src/app/paths.ts index 0c62c2b2..fe84d527 100644 --- a/src/app/paths.ts +++ b/src/app/paths.ts @@ -3,13 +3,13 @@ import { PhotoSetCategory } from '@/category'; import { getBaseUrl, GRID_HOMEPAGE_ENABLED } from './config'; import { Camera } from '@/camera'; import { parameterize } from '@/utility/string'; -import { TAG_HIDDEN } from '@/tag'; +import { TAG_PRIVATE } from '@/tag'; import { Lens } from '@/lens'; // Core export const PATH_ROOT = '/'; export const PATH_GRID = '/grid'; -export const PATH_FEED = '/feed'; +export const PATH_FULL = '/full'; export const PATH_ADMIN = '/admin'; export const PATH_API = '/api'; export const PATH_SIGN_IN = '/sign-in'; @@ -19,8 +19,8 @@ export const PATH_OG = '/og'; export const PATH_GRID_INFERRED = GRID_HOMEPAGE_ENABLED ? PATH_ROOT : PATH_GRID; -export const PATH_FEED_INFERRED = GRID_HOMEPAGE_ENABLED - ? PATH_FEED +export const PATH_FULL_INFERRED = GRID_HOMEPAGE_ENABLED + ? PATH_FULL : PATH_ROOT; // Sort @@ -31,7 +31,7 @@ export const PARAM_SORT_ORDER_OLDEST = 'oldest-first'; export const doesPathOfferSort = (pathname: string) => pathname === PATH_ROOT || pathname.startsWith(PATH_GRID) || - pathname.startsWith(PATH_FEED); + pathname.startsWith(PATH_FULL); // Feeds export const PATH_SITEMAP = '/sitemap.xml'; @@ -104,7 +104,7 @@ export const PATHS_ADMIN = [ export const PATHS_TO_CACHE = [ PATH_ROOT, PATH_GRID, - PATH_FEED, + PATH_FULL, PATH_OG, PATH_PHOTO_DYNAMIC, PATH_CAMERA_DYNAMIC, @@ -154,7 +154,7 @@ export const pathForPhoto = ({ let prefix = PREFIX_PHOTO; if (typeof photo !== 'string' && photo.hidden) { - prefix = pathForTag(TAG_HIDDEN); + prefix = pathForTag(TAG_PRIVATE); } else if (recent) { prefix = PREFIX_RECENTS; } else if (year) { @@ -231,13 +231,19 @@ export const pathForRecentsImage = () => pathForImage(PREFIX_RECENTS); // Absolute paths -export const ABSOLUTE_PATH_FOR_FEED_JSON = +export const ABSOLUTE_PATH_GRID = + `${getBaseUrl()}${PATH_GRID}`; + +export const ABSOLUTE_PATH_FULL = + `${getBaseUrl()}${PATH_FULL}`; + +export const ABSOLUTE_PATH_FEED_JSON = `${getBaseUrl()}${PATH_FEED_JSON}`; -export const ABSOLUTE_PATH_FOR_RSS_XML = +export const ABSOLUTE_PATH_RSS_XML = `${getBaseUrl()}${PATH_RSS_XML}`; -export const ABSOLUTE_PATH_FOR_HOME_IMAGE = +export const ABSOLUTE_PATH_HOME_IMAGE = `${getBaseUrl()}/home-image`; export const absolutePathForPhoto = ( @@ -374,13 +380,13 @@ export const isPathRoot = (pathname?: string) => export const isPathGrid = (pathname?: string) => checkPathPrefix(pathname, PATH_GRID); -export const isPathFeed = (pathname?: string) => - checkPathPrefix(pathname, PATH_FEED); +export const isPathFull = (pathname?: string) => + checkPathPrefix(pathname, PATH_FULL); export const isPathTopLevel = (pathname?: string) => isPathRoot(pathname)|| isPathGrid(pathname) || - isPathFeed(pathname); + isPathFull(pathname); export const isPathSignIn = (pathname?: string) => checkPathPrefix(pathname, PATH_SIGN_IN); @@ -406,7 +412,7 @@ export const isPathAdminInfo = (pathname?: string) => export const isPathProtected = (pathname?: string) => checkPathPrefix(pathname, PATH_ADMIN) || - checkPathPrefix(pathname, pathForTag(TAG_HIDDEN)) || + checkPathPrefix(pathname, pathForTag(TAG_PRIVATE)) || checkPathPrefix(pathname, PATH_OG); export const getPathComponents = (pathname = ''): { diff --git a/src/cmdk/CommandKClient.tsx b/src/cmdk/CommandKClient.tsx index 7bec66fb..2ddfa249 100644 --- a/src/cmdk/CommandKClient.tsx +++ b/src/cmdk/CommandKClient.tsx @@ -20,7 +20,7 @@ import { PATH_ADMIN_RECIPES, PATH_ADMIN_TAGS, PATH_ADMIN_UPLOADS, - PATH_FEED_INFERRED, + PATH_FULL_INFERRED, PATH_GRID_INFERRED, PATH_SIGN_IN, pathForCamera, @@ -50,10 +50,10 @@ import PhotoDate from '@/photo/PhotoDate'; import PhotoSmall from '@/photo/PhotoSmall'; import { FaCheck } from 'react-icons/fa6'; import { - addHiddenToTags, + addPrivateToTags, formatTag, isTagFavs, - isTagHidden, + isTagPrivate, limitTagsByCount, } from '@/tag'; import { formatCount, formatCountDescriptive } from '@/utility/string'; @@ -84,7 +84,6 @@ import useVisualViewportHeight from '@/utility/useVisualViewport'; import useMaskedScroll from '../components/useMaskedScroll'; import { labelForFilm } from '@/film'; import IconFavs from '@/components/icons/IconFavs'; -import IconHidden from '@/components/icons/IconHidden'; import { useAppText } from '@/i18n/state/client'; import LoaderButton from '@/components/primitives/LoaderButton'; import IconRecents from '@/components/icons/IconRecents'; @@ -311,12 +310,12 @@ export default function CommandKClient({ , [_years, queryLive]); const tags = useMemo(() => { - const tagsIncludingHidden = photosCountHidden > 0 - ? addHiddenToTags(_tags, photosCountHidden) + const tagsIncludingPrivate = photosCountHidden > 0 + ? addPrivateToTags(_tags, photosCountHidden) : _tags; return HIDE_TAGS_WITH_ONE_PHOTO - ? limitTagsByCount(tagsIncludingHidden, 2, queryLive) - : tagsIncludingHidden; + ? limitTagsByCount(tagsIncludingPrivate, 2, queryLive) + : tagsIncludingPrivate; }, [_tags, photosCountHidden, queryLive]); const categorySections: CommandKSection[] = useMemo(() => @@ -380,10 +379,10 @@ export default function CommandKClient({ className="translate-y-[-0.5px]" highlight />} - {isTagHidden(tag) && - } , annotation: formatCount(count), @@ -504,11 +503,11 @@ export default function CommandKClient({ }); } - const pageFeed: CommandKItem = { + const pageFull: CommandKItem = { label: GRID_HOMEPAGE_ENABLED - ? appText.nav.feed - : `${appText.nav.feed} (${appText.nav.home})`, - path: PATH_FEED_INFERRED, + ? appText.nav.full + : `${appText.nav.full} (${appText.nav.home})`, + path: PATH_FULL_INFERRED, }; const pageGrid: CommandKItem = { @@ -519,8 +518,8 @@ export default function CommandKClient({ }; const pageItems: CommandKItem[] = GRID_HOMEPAGE_ENABLED - ? [pageGrid, pageFeed] - : [pageFeed, pageGrid]; + ? [pageGrid, pageFull] + : [pageFull, pageGrid]; const sectionPages: CommandKSection = { heading: 'Pages', diff --git a/src/components/Checkbox.tsx b/src/components/Checkbox.tsx index 83cafa3c..dab4746f 100644 --- a/src/components/Checkbox.tsx +++ b/src/components/Checkbox.tsx @@ -2,10 +2,13 @@ import clsx from 'clsx/lite'; import { InputHTMLAttributes, ReactNode, RefObject } from 'react'; import { ImCheckmark } from 'react-icons/im'; +const SIZE = 'size-4.5'; + const boxStyles = clsx( 'relative', 'inline-flex items-center justify-center', - 'size-5 rounded-md border', + 'rounded-md border', + SIZE, ); export default function Checkbox({ @@ -22,7 +25,7 @@ export default function Checkbox({ {!hideLabel && @@ -124,16 +127,27 @@ export default function FieldSetWithStatus({ htmlFor={id} className={clsx( 'inline-flex flex-wrap gap-x-2 items-center select-none', - type === 'checkbox' && 'order-2 m-0', + type === 'checkbox' && 'order-2 m-0 translate-y-[0.25px]', + type === 'checkbox' && readOnly && + 'opacity-50 cursor-not-allowed', )} > - - {icon && - {icon} - } - {label} + + {icon && + + {icon} + } + + {label} + + {tooltip && + } {note && !error && : ; diff --git a/src/components/icons/IconLock.tsx b/src/components/icons/IconLock.tsx index 05a07c23..9716adc6 100644 --- a/src/components/icons/IconLock.tsx +++ b/src/components/icons/IconLock.tsx @@ -1,12 +1,19 @@ import { IconBaseProps } from 'react-icons'; -import { BiLockAlt } from 'react-icons/bi'; +import { BiLockAlt, BiLockOpenAlt } from 'react-icons/bi'; +import { FaLock, FaLockOpen } from 'react-icons/fa'; import { FiLock } from 'react-icons/fi'; export default function IconLock({ + solid, narrow, + open, ...props -}: IconBaseProps & { narrow?: boolean }) { - return narrow - ? - : ; +}: IconBaseProps & { solid?: boolean, narrow?: boolean, open?: boolean }) { + if (solid) { + return open ? : ; + } else if (narrow) { + return open ? : ; + } else { + return open ? : ; + } } diff --git a/src/feed/index.ts b/src/feed/index.ts index 97c4ae1e..77ed8e1e 100644 --- a/src/feed/index.ts +++ b/src/feed/index.ts @@ -1,32 +1,43 @@ -import { descriptionForPhoto, Photo, titleForPhoto } from '@/photo'; +import { USER_DEFAULT_SORT_OPTIONS } from '@/app/config'; +import { PhotoQueryOptions } from '../photo/db'; import { - getNextImageUrlForRequest, - NextImageSize, -} from '@/platforms/next-image'; + INFINITE_SCROLL_FULL_INITIAL, + INFINITE_SCROLL_GRID_INITIAL, +} from '../photo'; +import { SortBy } from '../photo/db/sort'; +import { FEED_PHOTO_REQUEST_LIMIT } from './programmatic'; -export const FEED_PHOTO_REQUEST_LIMIT = 40; +const FEED_BASE_QUERY_OPTIONS: PhotoQueryOptions = { + excludeFromFeeds: true, +}; -export const FEED_PHOTO_WIDTH_SMALL = 200; -export const FEED_PHOTO_WIDTH_MEDIUM = 640; -export const FEED_PHOTO_WIDTH_LARGE = 1200; +// PAGE FEED QUERY OPTIONS -export interface FeedMedia { - url: string - width: number - height: number -} - -export const generateFeedMedia = ( - photo: Photo, - size: NextImageSize, -): FeedMedia => ({ - url: getNextImageUrlForRequest({ imageUrl: photo.url, size }), - width: size, - height: Math.round(size / photo.aspectRatio), +export const getFeedQueryOptions = ({ + isGrid, + sortBy = USER_DEFAULT_SORT_OPTIONS.sortBy, + sortWithPriority = USER_DEFAULT_SORT_OPTIONS.sortWithPriority, +}: { + isGrid: boolean, + sortBy?: SortBy, + sortWithPriority?: boolean, +}): PhotoQueryOptions => ({ + ...FEED_BASE_QUERY_OPTIONS, + sortBy, + sortWithPriority, + limit: isGrid + ? INFINITE_SCROLL_GRID_INITIAL + : INFINITE_SCROLL_FULL_INITIAL, }); -export const getCoreFeedFields = (photo: Photo) => ({ - id: photo.id, - title: titleForPhoto(photo), - description: descriptionForPhoto(photo, true), -}); +export const FEED_META_QUERY_OPTIONS: PhotoQueryOptions = { + ...FEED_BASE_QUERY_OPTIONS, +}; + +// PROGRAMMATIC FEED QUERY OPTIONS + +export const PROGRAMMATIC_QUERY_OPTIONS: PhotoQueryOptions = { + ...FEED_BASE_QUERY_OPTIONS, + sortBy: 'createdAt', + limit: FEED_PHOTO_REQUEST_LIMIT, +}; diff --git a/src/feed/json.ts b/src/feed/json.ts index b9579ea9..0b29cab9 100644 --- a/src/feed/json.ts +++ b/src/feed/json.ts @@ -6,7 +6,7 @@ import { FeedMedia, generateFeedMedia, getCoreFeedFields, -} from '.'; +} from './programmatic'; import { formatDateFromPostgresString } from '@/utility/date'; import { Photo } from '@/photo'; import { BASE_URL, META_DESCRIPTION, META_TITLE } from '@/app/config'; diff --git a/src/feed/programmatic.ts b/src/feed/programmatic.ts new file mode 100644 index 00000000..97c4ae1e --- /dev/null +++ b/src/feed/programmatic.ts @@ -0,0 +1,32 @@ +import { descriptionForPhoto, Photo, titleForPhoto } from '@/photo'; +import { + getNextImageUrlForRequest, + NextImageSize, +} from '@/platforms/next-image'; + +export const FEED_PHOTO_REQUEST_LIMIT = 40; + +export const FEED_PHOTO_WIDTH_SMALL = 200; +export const FEED_PHOTO_WIDTH_MEDIUM = 640; +export const FEED_PHOTO_WIDTH_LARGE = 1200; + +export interface FeedMedia { + url: string + width: number + height: number +} + +export const generateFeedMedia = ( + photo: Photo, + size: NextImageSize, +): FeedMedia => ({ + url: getNextImageUrlForRequest({ imageUrl: photo.url, size }), + width: size, + height: Math.round(size / photo.aspectRatio), +}); + +export const getCoreFeedFields = (photo: Photo) => ({ + id: photo.id, + title: titleForPhoto(photo), + description: descriptionForPhoto(photo, true), +}); diff --git a/src/feed/rss.ts b/src/feed/rss.ts index 1fb975fe..cbd4882a 100644 --- a/src/feed/rss.ts +++ b/src/feed/rss.ts @@ -5,8 +5,8 @@ import { FeedMedia, generateFeedMedia, getCoreFeedFields, -} from '.'; -import { ABSOLUTE_PATH_FOR_RSS_XML, absolutePathForPhoto } from '@/app/paths'; +} from './programmatic'; +import { ABSOLUTE_PATH_RSS_XML, absolutePathForPhoto } from '@/app/paths'; import { formatDate } from '@/utility/date'; import { formatStringForXml } from '@/utility/string'; import { BASE_URL, META_DESCRIPTION, META_TITLE } from '@/app/config'; @@ -67,7 +67,7 @@ export const formatFeedRssXml = (photos: Photo[]) => ${META_TITLE} diff --git a/src/i18n/locales/bd-bn.ts b/src/i18n/locales/bd-bn.ts index 7bcfe4fd..70226d1a 100644 --- a/src/i18n/locales/bd-bn.ts +++ b/src/i18n/locales/bd-bn.ts @@ -44,7 +44,7 @@ export const TEXT: I18N = { }, nav: { home: 'হোম', - feed: 'ফিড', + full: 'সম্পূর্ণ', grid: 'গ্রিড', admin: 'অ্যাডমিন', search: 'সার্চ', @@ -103,8 +103,8 @@ export const TEXT: I18N = { edit: 'এডিট', favorite: 'পছন্দ', unfavorite: 'পছন্দ অপসারণ', - hide: 'লুকান', - unhide: 'দেখান', + private: 'ব্যক্তিগত করুন', + public: 'সর্বজনীন করুন', download: 'ডাউনলোড', sync: 'সিঙ্ক', delete: 'ডিলিট', diff --git a/src/i18n/locales/en-us.ts b/src/i18n/locales/en-us.ts index 0575a8d1..2242466b 100644 --- a/src/i18n/locales/en-us.ts +++ b/src/i18n/locales/en-us.ts @@ -42,7 +42,7 @@ export const TEXT = { }, nav: { home: 'Home', - feed: 'Feed', + full: 'Full', grid: 'Grid', admin: 'Admin', search: 'Search', @@ -101,8 +101,8 @@ export const TEXT = { edit: 'Edit', favorite: 'Favorite', unfavorite: 'Unfavorite', - hide: 'Hide', - unhide: 'Unhide', + private: 'Make Private', + public: 'Make Public', download: 'Download', sync: 'Sync', delete: 'Delete', diff --git a/src/i18n/locales/id-id.ts b/src/i18n/locales/id-id.ts index 887503c3..013f61c0 100644 --- a/src/i18n/locales/id-id.ts +++ b/src/i18n/locales/id-id.ts @@ -43,7 +43,7 @@ export const TEXT: I18N = { }, nav: { home: 'Beranda', - feed: 'Umpan', + full: 'Lengkap', grid: 'Grid', admin: 'Admin', search: 'Cari', @@ -102,8 +102,8 @@ export const TEXT: I18N = { edit: 'Edit', favorite: 'Favorit', unfavorite: 'Hapus dari Favorit', - hide: 'Sembunyikan', - unhide: 'Tampilkan', + private: 'Buat Privat', + public: 'Buat Publik', download: 'Unduh', sync: 'Sinkronkan', delete: 'Hapus', diff --git a/src/i18n/locales/pt-br.ts b/src/i18n/locales/pt-br.ts index 581f5686..bb3ace9a 100644 --- a/src/i18n/locales/pt-br.ts +++ b/src/i18n/locales/pt-br.ts @@ -43,7 +43,7 @@ export const TEXT: I18N = { }, nav: { home: 'Início', - feed: 'Feed', + full: 'Completo', grid: 'Grade', admin: 'Menu de administrador', search: 'Pesquisar', @@ -102,8 +102,8 @@ export const TEXT: I18N = { edit: 'Editar', favorite: 'Favoritar', unfavorite: 'Remover dos favoritos', - hide: 'Ocultar', - unhide: 'Mostrar', + private: 'Tornar Privado', + public: 'Tornar Público', download: 'Baixar', sync: 'Sincronizar', delete: 'Excluir', diff --git a/src/i18n/locales/pt-pt.ts b/src/i18n/locales/pt-pt.ts index 8a479ca2..af2811d3 100644 --- a/src/i18n/locales/pt-pt.ts +++ b/src/i18n/locales/pt-pt.ts @@ -43,7 +43,7 @@ export const TEXT: I18N = { }, nav: { home: 'Início', - feed: 'Feed', + full: 'Completo', grid: 'Grade', admin: 'Menu de administração', search: 'Pesquisar', @@ -102,8 +102,8 @@ export const TEXT: I18N = { edit: 'Editar', favorite: 'Favoritar', unfavorite: 'Remover dos favoritos', - hide: 'Ocultar', - unhide: 'Mostrar', + private: 'Tornar Privado', + public: 'Tornar Público', download: 'Descarregar', sync: 'Sincronizar', delete: 'Excluir', diff --git a/src/i18n/locales/zh-cn.ts b/src/i18n/locales/zh-cn.ts index 9d96876f..decc0cf6 100644 --- a/src/i18n/locales/zh-cn.ts +++ b/src/i18n/locales/zh-cn.ts @@ -43,7 +43,7 @@ export const TEXT: I18N = { }, nav: { home: '首页', - feed: '动态', + full: '完整', grid: '网格', admin: '管理', search: '搜索', @@ -102,8 +102,8 @@ export const TEXT: I18N = { edit: '编辑', favorite: '收藏', unfavorite: '取消收藏', - hide: '隐藏', - unhide: '取消隐藏', + private: '设为私密', + public: '设为公开', download: '下载', sync: '同步', delete: '删除', diff --git a/src/image-response/TemplateImageResponse.tsx b/src/image-response/TemplateImageResponse.tsx index 881d498b..52e61cb0 100644 --- a/src/image-response/TemplateImageResponse.tsx +++ b/src/image-response/TemplateImageResponse.tsx @@ -1,5 +1,5 @@ import { Photo } from '../photo'; -import IconFeed from '@/components/icons/IconFeed'; +import IconFull from '@/components/icons/IconFull'; import IconGrid from '@/components/icons/IconGrid'; import ImagePhotoGrid from './components/ImagePhotoGrid'; import { NextImageSize } from '@/platforms/next-image'; @@ -66,7 +66,7 @@ export default function TemplateImageResponse({ color: '#333', borderRight: '2px solid #333', }}> - +
}
); diff --git a/src/photo/PhotoGridContainer.tsx b/src/photo/PhotoGridContainer.tsx index ef0caaf6..c6c76745 100644 --- a/src/photo/PhotoGridContainer.tsx +++ b/src/photo/PhotoGridContainer.tsx @@ -15,6 +15,7 @@ export default function PhotoGridContainer({ count, sortBy, sortWithPriority, + excludeFromFeeds, animateOnFirstLoadOnly, header, sidebar, @@ -25,6 +26,7 @@ export default function PhotoGridContainer({ count: number sortBy?: SortBy sortWithPriority?: boolean + excludeFromFeeds?: boolean header?: ReactNode sidebar?: ReactNode } & ComponentProps) { @@ -61,6 +63,7 @@ export default function PhotoGridContainer({ initialOffset: photos.length, sortBy, sortWithPriority, + excludeFromFeeds, ...categories, canStart: shouldAnimateDynamicItems, animateOnFirstLoadOnly, diff --git a/src/photo/PhotoGridInfinite.tsx b/src/photo/PhotoGridInfinite.tsx index 7e608cb6..c1592d1d 100644 --- a/src/photo/PhotoGridInfinite.tsx +++ b/src/photo/PhotoGridInfinite.tsx @@ -11,6 +11,7 @@ export default function PhotoGridInfinite({ initialOffset, sortBy, sortWithPriority, + excludeFromFeeds, canStart, animateOnFirstLoadOnly, canSelect, @@ -20,6 +21,7 @@ export default function PhotoGridInfinite({ initialOffset: number sortBy?: SortBy sortWithPriority?: boolean + excludeFromFeeds?: boolean } & Omit, 'photos'>) { return ( {({ photos, onLastPhotoVisible }) => diff --git a/src/photo/PhotoGridPageClient.tsx b/src/photo/PhotoGridPageClient.tsx index c4ac4db4..7dfab9b1 100644 --- a/src/photo/PhotoGridPageClient.tsx +++ b/src/photo/PhotoGridPageClient.tsx @@ -42,6 +42,7 @@ export default function PhotoGridPageClient({ count={photosCount} sortBy={sortBy} sortWithPriority={sortWithPriority} + excludeFromFeeds prioritizeInitialPhotos sidebar={ - addHiddenToTags(tags, photosCountHidden) + addPrivateToTags(tags, photosCountHidden) , [tags, photosCountHidden]); const recentsContent = recents.length > 0 @@ -196,7 +201,7 @@ export default function PhotoGridSidebar({ .map(({ tag, count }) => { switch (tag) { case TAG_FAVS: - return ; - case TAG_HIDDEN: - return { - if (photo?.id) { return toggleHidePhotoAction(photo.id); } + if (photo?.id) { return togglePrivatePhotoAction(photo.id); } }, [photo?.id]); const navigateToPhotoEdit = useNavigateOrRunActionWithToast({ @@ -168,7 +168,7 @@ export default function PhotoPrevNextActions({ unfavoritePhoto(); } break; - case KEY_COMMANDS.toggleHide: + case KEY_COMMANDS.togglePrivate: if (isUserSignedIn && photo) { if (photo.hidden) { unhidePhoto(); diff --git a/src/photo/PhotosLargeInfinite.tsx b/src/photo/PhotosLargeInfinite.tsx index 9fcfacdf..3c1ffdbb 100644 --- a/src/photo/PhotosLargeInfinite.tsx +++ b/src/photo/PhotosLargeInfinite.tsx @@ -1,6 +1,6 @@ 'use client'; -import { PATH_FEED_INFERRED } from '@/app/paths'; +import { PATH_FULL_INFERRED } from '@/app/paths'; import InfinitePhotoScroll from './InfinitePhotoScroll'; import PhotosLarge from './PhotosLarge'; import { SortBy } from './db/sort'; @@ -17,7 +17,7 @@ export default function PhotosLargeInfinite({ }) { return ( diff --git a/src/photo/cache.ts b/src/photo/cache.ts index 981cbdeb..5fb08ee9 100644 --- a/src/photo/cache.ts +++ b/src/photo/cache.ts @@ -25,7 +25,7 @@ import { PATHS_ADMIN, PATHS_TO_CACHE, PATH_ADMIN, - PATH_FEED, + PATH_FULL, PATH_GRID, PATH_ROOT, PREFIX_CAMERA, @@ -153,7 +153,7 @@ export const revalidatePhoto = (photoId: string) => { revalidatePath(pathForPhoto({ photo: photoId }), 'layout'); revalidatePath(PATH_ROOT, 'layout'); revalidatePath(PATH_GRID, 'layout'); - revalidatePath(PATH_FEED, 'layout'); + revalidatePath(PATH_FULL, 'layout'); revalidatePath(PREFIX_TAG, 'layout'); revalidatePath(PREFIX_CAMERA, 'layout'); revalidatePath(PREFIX_LENS, 'layout'); @@ -179,18 +179,23 @@ export const getPhotosNearIdCached = ( getPhotosNearId, [KEY_PHOTOS, ...getPhotosCacheKeys(args[1])], )(...args).then(({ photos, indexNumber }) => { - const [photoId, { limit }] = args; + const [photoId, { limit }, excludeFromFeeds] = args; const photo = photos.find(({ id }) => id === photoId); const isPhotoFirst = photos.findIndex(p => p.id === photoId) === 0; return { photo: photo ? parseCachedPhotoDates(photo) : undefined, - photos: parseCachedPhotosDates(photos), - ...limit && { - photosGrid: photos.slice( - isPhotoFirst ? 1 : 2, - isPhotoFirst ? limit - 1 : limit, - ), - }, + // Don't show photo in context when excluded from feeds + ...excludeFromFeeds && photo?.excludeFromFeeds + ? { photos: [] } + : { + photos: parseCachedPhotosDates(photos), + ...limit && { + photosGrid: photos.slice( + isPhotoFirst ? 1 : 2, + isPhotoFirst ? limit - 1 : limit, + ), + }, + }, indexNumber, }; }); diff --git a/src/photo/db/index.ts b/src/photo/db/index.ts index 2a555393..4c3a8063 100644 --- a/src/photo/db/index.ts +++ b/src/photo/db/index.ts @@ -29,6 +29,7 @@ export type PhotoQueryOptions = { takenBefore?: Date takenAfterInclusive?: Date updatedBefore?: Date + excludeFromFeeds?: boolean hidden?: 'exclude' | 'include' | 'only' } & Omit & { camera?: Partial @@ -44,6 +45,7 @@ export const getWheresFromOptions = ( ) => { const { hidden = 'exclude', + excludeFromFeeds, takenBefore, takenAfterInclusive, updatedBefore, @@ -72,6 +74,9 @@ export const getWheresFromOptions = ( break; } + if (excludeFromFeeds) { + wheres.push('exclude_from_feeds IS NOT TRUE'); + } if (takenBefore) { wheres.push(`taken_at < $${valuesIndex++}`); wheresValues.push(takenBefore.toISOString()); diff --git a/src/photo/db/migration.ts b/src/photo/db/migration.ts index 2dbeb765..d44b2956 100644 --- a/src/photo/db/migration.ts +++ b/src/photo/db/migration.ts @@ -71,6 +71,13 @@ export const MIGRATIONS: Migration[] = [{ END IF; END $$; `, +}, { + label: '06: Exclude from feeds', + fields: ['exclude_from_feeds'], + run: () => sql` + ALTER TABLE photos + ADD COLUMN IF NOT EXISTS exclude_from_feeds BOOLEAN DEFAULT FALSE + `, }]; export const migrationForError = (e: any) => diff --git a/src/photo/db/query.ts b/src/photo/db/query.ts index 3deab5de..2442a7f3 100644 --- a/src/photo/db/query.ts +++ b/src/photo/db/query.ts @@ -69,7 +69,8 @@ const createPhotosTable = () => priority_order REAL, taken_at TIMESTAMP WITH TIME ZONE NOT NULL, taken_at_naive VARCHAR(255) NOT NULL, - hidden BOOLEAN, + exclude_from_feeds BOOLEAN DEFAULT FALSE, + hidden BOOLEAN DEFAULT FALSE, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ) @@ -193,6 +194,7 @@ export const insertPhoto = (photo: PhotoDbInsert) => recipe_title, recipe_data, priority_order, + exclude_from_feeds, hidden, taken_at, taken_at_naive @@ -224,6 +226,7 @@ export const insertPhoto = (photo: PhotoDbInsert) => ${photo.recipeTitle}, ${photo.recipeData}, ${photo.priorityOrder}, + ${photo.excludeFromFeeds}, ${photo.hidden}, ${photo.takenAt}, ${photo.takenAtNaive} @@ -258,6 +261,7 @@ export const updatePhoto = (photo: PhotoDbInsert) => recipe_title=${photo.recipeTitle}, recipe_data=${photo.recipeData}, priority_order=${photo.priorityOrder || null}, + exclude_from_feeds=${photo.excludeFromFeeds}, hidden=${photo.hidden}, taken_at=${photo.takenAt}, taken_at_naive=${photo.takenAtNaive}, @@ -527,6 +531,7 @@ export const getPhotos = async (options: PhotoQueryOptions = {}) => export const getPhotosNearId = async ( photoId: string, options: PhotoQueryOptions, + excludeFromFeeds?: boolean, ) => safelyQueryPhotos(async () => { const { limit } = options; @@ -561,6 +566,7 @@ export const getPhotosNearId = async ( return { photos: rows.map(parsePhotoFromDb), indexNumber, + excludeFromFeeds, }; }); }, `getPhotosNearId: ${photoId}`); diff --git a/src/photo/db/sort-path.ts b/src/photo/db/sort-path.ts index 1ae9c38e..5b0d02a9 100644 --- a/src/photo/db/sort-path.ts +++ b/src/photo/db/sort-path.ts @@ -6,8 +6,8 @@ import { PARAM_SORT_ORDER_OLDEST, PARAM_SORT_TYPE_TAKEN_AT, PARAM_SORT_TYPE_UPLOADED_AT, - PATH_FEED, - PATH_FEED_INFERRED, + PATH_FULL, + PATH_FULL_INFERRED, PATH_GRID, PATH_GRID_INFERRED, } from '@/app/paths'; @@ -84,11 +84,11 @@ export const getSortOptionsFromParams = async ( }; export const getPathSortComponents = (pathname: string) => { - const [_, gridOrFeed, sortType, sortOrder] = pathname.split('/'); + const [_, gridOrFull, sortType, sortOrder] = pathname.split('/'); return { - gridOrFeed: gridOrFeed || (GRID_HOMEPAGE_ENABLED + gridOrFull: gridOrFull || (GRID_HOMEPAGE_ENABLED ? 'grid' - : 'feed' + : 'full' ), sortType: sortType || DEFAULT_SORT_TYPE, sortOrder: sortOrder || DEFAULT_SORT_ORDER, @@ -101,7 +101,7 @@ const getReversedSortOrder = (sortOrder: string): string => : PARAM_SORT_ORDER_OLDEST; export const getSortConfigFromPath = (pathname: string) => { - const { gridOrFeed, sortType, sortOrder } = getPathSortComponents(pathname); + const { gridOrFull, sortType, sortOrder } = getPathSortComponents(pathname); const { sortBy } = _getSortOptionsFromParams(sortType, sortOrder); const isSortedByDefault = sortBy === USER_DEFAULT_SORT_BY; const reversedSortOrder = getReversedSortOrder(sortOrder); @@ -116,13 +116,13 @@ export const getSortConfigFromPath = (pathname: string) => { pathGrid: isSortedByDefault ? PATH_GRID_INFERRED : `${PATH_GRID}/${sortType}/${sortOrder}`, - pathFeed: isSortedByDefault - ? PATH_FEED_INFERRED - : `${PATH_FEED}/${sortType}/${sortOrder}`, + pathFull: isSortedByDefault + ? PATH_FULL_INFERRED + : `${PATH_FULL}/${sortType}/${sortOrder}`, pathSort: doesReverseSortMatchDefault - ? gridOrFeed === 'grid' + ? gridOrFull === 'grid' ? PATH_GRID_INFERRED - : PATH_FEED_INFERRED - : `/${gridOrFeed}/${sortType}/${reversedSortOrder}`, + : PATH_FULL_INFERRED + : `/${gridOrFull}/${sortType}/${reversedSortOrder}`, }; }; diff --git a/src/photo/form/FieldsetHidden.tsx b/src/photo/form/FieldsetExclude.tsx similarity index 54% rename from src/photo/form/FieldsetHidden.tsx rename to src/photo/form/FieldsetExclude.tsx index 226a7c27..1ce67adf 100644 --- a/src/photo/form/FieldsetHidden.tsx +++ b/src/photo/form/FieldsetExclude.tsx @@ -2,16 +2,21 @@ import { ComponentProps } from 'react'; import FieldSetWithStatus from '@/components/FieldSetWithStatus'; import IconHidden from '@/components/icons/IconHidden'; -export default function FieldsetHidden(props: Omit< +export default function FieldsetExclude(props: Omit< ComponentProps, 'label' | 'icon' | 'type' >) { return ( } + icon={} + tooltip="Do not show on homepage views or RSS" /> ); } diff --git a/src/photo/form/FieldsetPrivate.tsx b/src/photo/form/FieldsetPrivate.tsx new file mode 100644 index 00000000..c6eff956 --- /dev/null +++ b/src/photo/form/FieldsetPrivate.tsx @@ -0,0 +1,22 @@ +import { ComponentProps } from 'react'; +import FieldSetWithStatus from '@/components/FieldSetWithStatus'; +import IconLock from '@/components/icons/IconLock'; + +export default function FieldsetPrivate(props: Omit< + ComponentProps, + 'label' | 'icon' | 'type' +>) { + return ( + } + tooltip="Visible only to authenticated admin" + /> + ); +} diff --git a/src/photo/form/PhotoForm.tsx b/src/photo/form/PhotoForm.tsx index 46fafe7f..68780b6a 100644 --- a/src/photo/form/PhotoForm.tsx +++ b/src/photo/form/PhotoForm.tsx @@ -45,9 +45,10 @@ import { convertFilmsForForm, Films } from '@/film'; import { isMakeFujifilm } from '@/platforms/fujifilm'; import PhotoFilmIcon from '@/film/PhotoFilmIcon'; import FieldsetFavs from './FieldsetFavs'; -import FieldsetHidden from './FieldsetHidden'; +import FieldsetPrivate from './FieldsetPrivate'; import { useAppText } from '@/i18n/state/client'; import IconAddUpload from '@/components/icons/IconAddUpload'; +import FieldsetExclude from './FieldsetExclude'; const THUMBNAIL_SIZE = 300; @@ -184,6 +185,16 @@ export default function PhotoForm({ onTextContentChange?.(formHasTextContent(formData)); }, [onTextContentChange, formData]); + useEffect(() => { + if (formData.hidden === 'true') { + setFormData(data => ({ + ...data, + excludeFromFeeds: 'false', + favorite: 'false', + })); + } + }, [formData.hidden]); + const isFieldGeneratingAi = (key: keyof PhotoFormData) => { switch (key) { case 'title': @@ -241,7 +252,7 @@ export default function PhotoForm({ } }; - const shouldHideField = ( + const isFieldHidden = ( key: FormFields, hideIfEmpty?: boolean, shouldHide?: FormMeta['shouldHide'], @@ -261,6 +272,13 @@ export default function PhotoForm({ } }; + const isFieldReadOnly = (key: FormFields) => { + return formData.hidden === 'true' && ( + key === 'excludeFromFeeds' || + key === 'favorite' + ); + }; + const onMatchResults = useCallback((didFindMatchingPhotos: boolean) => { setFormData(data => ({ ...data, @@ -361,7 +379,7 @@ export default function PhotoForm({ type, staticValue, }]) => { - if (!shouldHideField(key, hideIfEmpty, shouldHide)) { + if (!isFieldHidden(key, hideIfEmpty, shouldHide)) { const fieldProps: ComponentProps = { id: key, label: label + ( @@ -403,7 +421,7 @@ export default function PhotoForm({ tagOptionsLimit, tagOptionsLimitValidationMessage, required, - readOnly, + readOnly: readOnly || isFieldReadOnly(key), spellCheck, capitalize, placeholder: loadingMessage && !formData[key] @@ -445,8 +463,13 @@ export default function PhotoForm({ key={key} {...fieldProps} />; + case 'excludeFromFeeds': + return ; case 'hidden': - return ; @@ -462,7 +485,7 @@ export default function PhotoForm({ {/* Actions */}
{ if (photos.length > 0) { return { openGraph: { - images: ABSOLUTE_PATH_FOR_HOME_IMAGE, + images: ABSOLUTE_PATH_HOME_IMAGE, }, twitter: { card: 'summary_large_image', - images: ABSOLUTE_PATH_FOR_HOME_IMAGE, + images: ABSOLUTE_PATH_HOME_IMAGE, }, }; } else { diff --git a/src/photo/key-commands.ts b/src/photo/key-commands.ts index 32b25724..40539261 100644 --- a/src/photo/key-commands.ts +++ b/src/photo/key-commands.ts @@ -1,5 +1,5 @@ export const KEY_COMMANDS = { - feed: 'F', + full: 'F', grid: 'G', admin: 'A', prev: ['J', 'ARROWLEFT'], @@ -7,7 +7,7 @@ export const KEY_COMMANDS = { edit: 'E', favorite: 'P', unfavorite: 'X', - toggleHide: 'H', + togglePrivate: 'M', download: 'D', sync: 'S', search: ['⌘', 'K'], diff --git a/src/tag/HiddenTag.tsx b/src/tag/HiddenTag.tsx deleted file mode 100644 index 9dc29a9e..00000000 --- a/src/tag/HiddenTag.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { TAG_HIDDEN } from '.'; -import { pathForTag } from '@/app/paths'; -import IconHidden from '@/components/icons/IconHidden'; -import EntityLink, { - EntityLinkExternalProps, -} from '@/components/entity/EntityLink'; - -export default function HiddenTag(props: EntityLinkExternalProps) { - return ( - } - iconBadgeEnd={} - /> - ); -} diff --git a/src/tag/FavsTag.tsx b/src/tag/PhotoFavs.tsx similarity index 90% rename from src/tag/FavsTag.tsx rename to src/tag/PhotoFavs.tsx index 163fc40e..4b47cb3f 100644 --- a/src/tag/FavsTag.tsx +++ b/src/tag/PhotoFavs.tsx @@ -7,7 +7,7 @@ import EntityLink, { } from '@/components/entity/EntityLink'; import IconFavs from '@/components/icons/IconFavs'; -export default function FavsTag(props: EntityLinkExternalProps) { +export default function PhotoFavs(props: EntityLinkExternalProps) { return ( } + iconBadgeEnd={} + /> + ); +} diff --git a/src/tag/PhotoTags.tsx b/src/tag/PhotoTags.tsx index 047ee271..c7a675c8 100644 --- a/src/tag/PhotoTags.tsx +++ b/src/tag/PhotoTags.tsx @@ -1,6 +1,6 @@ import PhotoTag from '@/tag/PhotoTag'; import { isTagFavs } from '.'; -import FavsTag from './FavsTag'; +import PhotoFavs from './PhotoFavs'; import { EntityLinkExternalProps } from '@/components/entity/EntityLink'; import { Fragment } from 'react'; @@ -18,7 +18,7 @@ export default function PhotoTags({ {tags.map(tag => {isTagFavs(tag) - ? } + entity={} entityDescription={photoQuantityText(count, appText, false, false)} photos={photos} selectedPhoto={selectedPhoto} diff --git a/src/tag/TagHeader.tsx b/src/tag/TagHeader.tsx index 6991248e..116f1059 100644 --- a/src/tag/TagHeader.tsx +++ b/src/tag/TagHeader.tsx @@ -2,7 +2,7 @@ import { Photo, PhotoDateRange } from '@/photo'; import PhotoTag from './PhotoTag'; import { descriptionForTaggedPhotos, isTagFavs } from '.'; import PhotoHeader from '@/photo/PhotoHeader'; -import FavsTag from './FavsTag'; +import PhotoFavs from './PhotoFavs'; import { AI_TEXT_GENERATION_ENABLED } from '@/app/config'; import { getAppText } from '@/i18n/state/server'; @@ -26,7 +26,7 @@ export default async function TagHeader({ diff --git a/src/tag/index.ts b/src/tag/index.ts index 5d60e8a5..89e299ed 100644 --- a/src/tag/index.ts +++ b/src/tag/index.ts @@ -20,7 +20,7 @@ import { AppTextState } from '@/i18n/state'; // Reserved tags export const TAG_FAVS = 'favs'; -export const TAG_HIDDEN = 'hidden'; +export const TAG_PRIVATE = 'private'; type TagWithMeta = { tag: string } & CategoryQueryMeta; @@ -31,7 +31,7 @@ export const formatTag = (tag?: string) => export const getValidationMessageForTags = (tags?: string) => { const reservedTags = (convertStringToArray(tags) ?? []) - .filter(tag => isTagFavs(tag) || isTagHidden(tag)) + .filter(tag => isTagFavs(tag) || isTagPrivate(tag)) .map(tag => tag.toLocaleUpperCase()); return reservedTags.length ? `Reserved tags: ${reservedTags.join(', ').toLocaleLowerCase()}` @@ -135,20 +135,20 @@ export const isPhotoFav = ({ tags }: Photo) => tags.some(isTagFavs); export const isPathFavs = (pathname?: string) => getPathComponents(pathname).tag === TAG_FAVS; -export const isTagHidden = (tag: string) => tag.toLowerCase() === TAG_HIDDEN; +export const isTagPrivate = (tag: string) => tag.toLowerCase() === TAG_PRIVATE; -export const addHiddenToTags = ( +export const addPrivateToTags = ( tags: Tags, - countHidden = 0, - lastModifiedHidden = new Date(), + countPrivate = 0, + lastModifiedPrivate = new Date(), ) => - countHidden > 0 + countPrivate > 0 ? tags .filter(({ tag }) => tag === TAG_FAVS) .concat({ - tag: TAG_HIDDEN, - count: countHidden, - lastModified: lastModifiedHidden, + tag: TAG_PRIVATE, + count: countPrivate, + lastModified: lastModifiedPrivate, }) .concat(tags .filter(({ tag }) => tag !== TAG_FAVS) @@ -176,7 +176,7 @@ export const limitTagsByCount = ( tags.filter(({ tag, count }) => ( count >= minimumCount || isTagFavs(tag) || - isTagHidden(tag) || + isTagPrivate(tag) || (queryToInclude && tag .toLocaleLowerCase() .includes(queryToInclude.toLocaleLowerCase()))