Exclude photo from feeds (#280)
* Add tooltip to 'hidden' checkbox * Refine checkbox UI * Allow photos to be excluded from main feeds * Fix footer grid in photos excluded from feed * Apply feed exclusion from batch upload * Scrub final hidden/private language * Add visibility icons to admin photo menu
This commit is contained in:
parent
bf78f786a7
commit
70f6f48044
@ -146,14 +146,14 @@ Application behavior can be changed by configuring the following environment var
|
|||||||
|
|
||||||
#### Sorting
|
#### Sorting
|
||||||
- `NEXT_PUBLIC_DEFAULT_SORT`
|
- `NEXT_PUBLIC_DEFAULT_SORT`
|
||||||
- Sets default sort on grid/feed homepages
|
- Sets default sort on grid/full homepages
|
||||||
- Accepted values:
|
- Accepted values:
|
||||||
- `taken-at` (default)
|
- `taken-at` (default)
|
||||||
- `taken-at-oldest-first`
|
- `taken-at-oldest-first`
|
||||||
- `uploaded-at`
|
- `uploaded-at`
|
||||||
- `uploaded-at-oldest-first`
|
- `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_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
|
#### Display
|
||||||
- `NEXT_PUBLIC_HIDE_KEYBOARD_SHORTCUT_TOOLTIPS = 1` hides keyboard shortcut hints in areas like the main nav, and previous/next photo links
|
- `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?
|
#### 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`.
|
> 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?”
|
#### How secure are photos marked “private?”
|
||||||
> 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.
|
> 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?
|
#### 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."
|
> Navigate to `/admin/configuration` and click "Clear Cache."
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import {
|
|||||||
isPathTag,
|
isPathTag,
|
||||||
isPathTagPhoto,
|
isPathTagPhoto,
|
||||||
} from '@/app/paths';
|
} from '@/app/paths';
|
||||||
import { TAG_HIDDEN } from '@/tag';
|
import { TAG_PRIVATE } from '@/tag';
|
||||||
|
|
||||||
const PHOTO_ID = 'UsKSGcbt';
|
const PHOTO_ID = 'UsKSGcbt';
|
||||||
const TAG = 'tag-name';
|
const TAG = 'tag-name';
|
||||||
@ -25,7 +25,7 @@ const FOCAL_LENGTH_STRING = `${FOCAL_LENGTH}mm`;
|
|||||||
|
|
||||||
const PATH_ROOT = '/';
|
const PATH_ROOT = '/';
|
||||||
const PATH_GRID = '/grid';
|
const PATH_GRID = '/grid';
|
||||||
const PATH_FEED = '/feed';
|
const PATH_FULL = '/full';
|
||||||
const PATH_ADMIN = '/admin/photos';
|
const PATH_ADMIN = '/admin/photos';
|
||||||
const PATH_OG = '/og';
|
const PATH_OG = '/og';
|
||||||
const PATH_OG_ALL = `${PATH_OG}/all`;
|
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 = `/tag/${TAG}`;
|
||||||
const PATH_TAG_PHOTO = `${PATH_TAG}/${PHOTO_ID}`;
|
const PATH_TAG_PHOTO = `${PATH_TAG}/${PHOTO_ID}`;
|
||||||
|
|
||||||
const PATH_TAG_HIDDEN = `/tag/${TAG_HIDDEN}`;
|
const PATH_TAG_PRIVATE = `/tag/${TAG_PRIVATE}`;
|
||||||
const PATH_TAG_HIDDEN_PHOTO = `${PATH_TAG_HIDDEN}/${PHOTO_ID}`;
|
const PATH_TAG_PRIVATE_PHOTO = `${PATH_TAG_PRIVATE}/${PHOTO_ID}`;
|
||||||
|
|
||||||
const PATH_CAMERA = `/shot-on/${CAMERA_MAKE}/${CAMERA_MODEL}`;
|
const PATH_CAMERA = `/shot-on/${CAMERA_MAKE}/${CAMERA_MODEL}`;
|
||||||
const PATH_CAMERA_PHOTO = `${PATH_CAMERA}/${PHOTO_ID}`;
|
const PATH_CAMERA_PHOTO = `${PATH_CAMERA}/${PHOTO_ID}`;
|
||||||
@ -62,8 +62,8 @@ describe('Paths', () => {
|
|||||||
expect(isPathProtected(PATH_OG)).toBe(true);
|
expect(isPathProtected(PATH_OG)).toBe(true);
|
||||||
expect(isPathProtected(PATH_OG_ALL)).toBe(true);
|
expect(isPathProtected(PATH_OG_ALL)).toBe(true);
|
||||||
expect(isPathProtected(PATH_OG_SAMPLE)).toBe(true);
|
expect(isPathProtected(PATH_OG_SAMPLE)).toBe(true);
|
||||||
expect(isPathProtected(PATH_TAG_HIDDEN)).toBe(true);
|
expect(isPathProtected(PATH_TAG_PRIVATE)).toBe(true);
|
||||||
expect(isPathProtected(PATH_TAG_HIDDEN_PHOTO)).toBe(true);
|
expect(isPathProtected(PATH_TAG_PRIVATE_PHOTO)).toBe(true);
|
||||||
});
|
});
|
||||||
it('can be classified', () => {
|
it('can be classified', () => {
|
||||||
// Positive
|
// Positive
|
||||||
@ -123,7 +123,7 @@ describe('Paths', () => {
|
|||||||
// Root
|
// Root
|
||||||
expect(getEscapePath(PATH_ROOT)).toEqual(undefined);
|
expect(getEscapePath(PATH_ROOT)).toEqual(undefined);
|
||||||
expect(getEscapePath(PATH_GRID)).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);
|
expect(getEscapePath(PATH_ADMIN)).toEqual(undefined);
|
||||||
// Photo
|
// Photo
|
||||||
expect(getEscapePath(PATH_PHOTO)).toEqual(PATH_ROOT);
|
expect(getEscapePath(PATH_PHOTO)).toEqual(PATH_ROOT);
|
||||||
|
|||||||
@ -1,17 +1,15 @@
|
|||||||
import { getPhotosCached } from '@/photo/cache';
|
import { getPhotosCached } from '@/photo/cache';
|
||||||
import { SITE_FEEDS_ENABLED } from '@/app/config';
|
import { SITE_FEEDS_ENABLED } from '@/app/config';
|
||||||
import { FEED_PHOTO_REQUEST_LIMIT } from '@/feed';
|
|
||||||
import { formatFeedJson } from '@/feed/json';
|
import { formatFeedJson } from '@/feed/json';
|
||||||
|
import { PROGRAMMATIC_QUERY_OPTIONS } from '@/feed';
|
||||||
|
|
||||||
// Cache for 24 hours
|
// Cache for 24 hours
|
||||||
export const revalidate = 86_400;
|
export const revalidate = 86_400;
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
if (SITE_FEEDS_ENABLED) {
|
if (SITE_FEEDS_ENABLED) {
|
||||||
const photos = await getPhotosCached({
|
const photos = await getPhotosCached(PROGRAMMATIC_QUERY_OPTIONS)
|
||||||
limit: FEED_PHOTO_REQUEST_LIMIT,
|
.catch(() => []);
|
||||||
sortBy: 'createdAt',
|
|
||||||
}).catch(() => []);
|
|
||||||
return Response.json(formatFeedJson(photos));
|
return Response.json(formatFeedJson(photos));
|
||||||
} else {
|
} else {
|
||||||
return new Response('Feeds disabled', { status: 404 });
|
return new Response('Feeds disabled', { status: 404 });
|
||||||
|
|||||||
@ -1,52 +1,51 @@
|
|||||||
import {
|
import { generateOgImageMetaForPhotos } from '@/photo';
|
||||||
INFINITE_SCROLL_FEED_INITIAL,
|
|
||||||
generateOgImageMetaForPhotos,
|
|
||||||
} from '@/photo';
|
|
||||||
import PhotosEmptyState from '@/photo/PhotosEmptyState';
|
import PhotosEmptyState from '@/photo/PhotosEmptyState';
|
||||||
import { Metadata } from 'next/types';
|
import { Metadata } from 'next/types';
|
||||||
import { cache } from 'react';
|
import { cache } from 'react';
|
||||||
import { getPhotos } from '@/photo/db/query';
|
import { getPhotos } from '@/photo/db/query';
|
||||||
import PhotoFeedPage from '@/photo/PhotoFeedPage';
|
import PhotoFullPage from '@/photo/PhotoFullPage';
|
||||||
import { getPhotosMetaCached } from '@/photo/cache';
|
import { getPhotosMetaCached } from '@/photo/cache';
|
||||||
import { SortProps } from '@/photo/db/sort';
|
import { SortProps } from '@/photo/db/sort';
|
||||||
import { getSortOptionsFromParams } from '@/photo/db/sort-path';
|
import { getSortOptionsFromParams } from '@/photo/db/sort-path';
|
||||||
import { PhotoQueryOptions } from '@/photo/db';
|
import { PhotoQueryOptions } from '@/photo/db';
|
||||||
|
import { FEED_META_QUERY_OPTIONS, getFeedQueryOptions } from '@/feed';
|
||||||
|
|
||||||
export const maxDuration = 60;
|
export const maxDuration = 60;
|
||||||
|
|
||||||
const getPhotosCached = cache((options: PhotoQueryOptions) => getPhotos({
|
const getPhotosCached = cache((options: PhotoQueryOptions) =>
|
||||||
|
getPhotos(getFeedQueryOptions({
|
||||||
|
isGrid: false,
|
||||||
...options,
|
...options,
|
||||||
limit: INFINITE_SCROLL_FEED_INITIAL,
|
})));
|
||||||
}));
|
|
||||||
|
|
||||||
export async function generateMetadata({
|
export async function generateMetadata({
|
||||||
params,
|
params,
|
||||||
}: SortProps): Promise<Metadata> {
|
}: SortProps): Promise<Metadata> {
|
||||||
const options = await getSortOptionsFromParams(params);
|
const sortOptions = await getSortOptionsFromParams(params);
|
||||||
const photos = await getPhotosCached(options)
|
const photos = await getPhotosCached(sortOptions)
|
||||||
.catch(() => []);
|
.catch(() => []);
|
||||||
return generateOgImageMetaForPhotos(photos);
|
return generateOgImageMetaForPhotos(photos);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function FeedPageSort({ params }: SortProps) {
|
export default async function FullPageSort({ params }: SortProps) {
|
||||||
const options = await getSortOptionsFromParams(params);
|
const sortOptions = await getSortOptionsFromParams(params);
|
||||||
const [
|
const [
|
||||||
photos,
|
photos,
|
||||||
photosCount,
|
photosCount,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
getPhotosCached(options)
|
getPhotosCached(sortOptions)
|
||||||
.catch(() => []),
|
.catch(() => []),
|
||||||
getPhotosMetaCached(options)
|
getPhotosMetaCached(FEED_META_QUERY_OPTIONS)
|
||||||
.then(({ count }) => count)
|
.then(({ count }) => count)
|
||||||
.catch(() => 0),
|
.catch(() => 0),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
photos.length > 0
|
photos.length > 0
|
||||||
? <PhotoFeedPage {...{
|
? <PhotoFullPage {...{
|
||||||
photos,
|
photos,
|
||||||
photosCount,
|
photosCount,
|
||||||
...options,
|
...sortOptions,
|
||||||
}} />
|
}} />
|
||||||
: <PhotosEmptyState />
|
: <PhotosEmptyState />
|
||||||
);
|
);
|
||||||
@ -1,45 +1,41 @@
|
|||||||
import {
|
import { generateOgImageMetaForPhotos } from '@/photo';
|
||||||
INFINITE_SCROLL_FEED_INITIAL,
|
|
||||||
generateOgImageMetaForPhotos,
|
|
||||||
} from '@/photo';
|
|
||||||
import PhotosEmptyState from '@/photo/PhotosEmptyState';
|
import PhotosEmptyState from '@/photo/PhotosEmptyState';
|
||||||
import { Metadata } from 'next/types';
|
import { Metadata } from 'next/types';
|
||||||
import { cache } from 'react';
|
import { cache } from 'react';
|
||||||
import { getPhotos } from '@/photo/db/query';
|
import { getPhotos } from '@/photo/db/query';
|
||||||
import PhotoFeedPage from '@/photo/PhotoFeedPage';
|
import PhotoFullPage from '@/photo/PhotoFullPage';
|
||||||
import { getPhotosMetaCached } from '@/photo/cache';
|
import { getPhotosMetaCached } from '@/photo/cache';
|
||||||
import { USER_DEFAULT_SORT_OPTIONS } from '@/app/config';
|
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 dynamic = 'force-static';
|
||||||
export const maxDuration = 60;
|
export const maxDuration = 60;
|
||||||
|
|
||||||
const getPhotosCached = cache((options: PhotoQueryOptions) => getPhotos({
|
const getPhotosCached = cache(() => getPhotos(getFeedQueryOptions({
|
||||||
...options,
|
isGrid: false,
|
||||||
limit: INFINITE_SCROLL_FEED_INITIAL,
|
})));
|
||||||
}));
|
|
||||||
|
|
||||||
export async function generateMetadata(): Promise<Metadata> {
|
export async function generateMetadata(): Promise<Metadata> {
|
||||||
const photos = await getPhotosCached(USER_DEFAULT_SORT_OPTIONS)
|
const photos = await getPhotosCached()
|
||||||
.catch(() => []);
|
.catch(() => []);
|
||||||
return generateOgImageMetaForPhotos(photos);
|
return generateOgImageMetaForPhotos(photos);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function FeedPage() {
|
export default async function FullPage() {
|
||||||
const [
|
const [
|
||||||
photos,
|
photos,
|
||||||
photosCount,
|
photosCount,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
getPhotosCached(USER_DEFAULT_SORT_OPTIONS)
|
getPhotosCached()
|
||||||
.catch(() => []),
|
.catch(() => []),
|
||||||
getPhotosMetaCached(USER_DEFAULT_SORT_OPTIONS)
|
getPhotosMetaCached(FEED_META_QUERY_OPTIONS)
|
||||||
.then(({ count }) => count)
|
.then(({ count }) => count)
|
||||||
.catch(() => 0),
|
.catch(() => 0),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
photos.length > 0
|
photos.length > 0
|
||||||
? <PhotoFeedPage {...{
|
? <PhotoFullPage {...{
|
||||||
photos,
|
photos,
|
||||||
photosCount,
|
photosCount,
|
||||||
...USER_DEFAULT_SORT_OPTIONS,
|
...USER_DEFAULT_SORT_OPTIONS,
|
||||||
@ -1,7 +1,4 @@
|
|||||||
import {
|
import { generateOgImageMetaForPhotos } from '@/photo';
|
||||||
INFINITE_SCROLL_GRID_INITIAL,
|
|
||||||
generateOgImageMetaForPhotos,
|
|
||||||
} from '@/photo';
|
|
||||||
import PhotosEmptyState from '@/photo/PhotosEmptyState';
|
import PhotosEmptyState from '@/photo/PhotosEmptyState';
|
||||||
import { Metadata } from 'next/types';
|
import { Metadata } from 'next/types';
|
||||||
import { getPhotos } from '@/photo/db/query';
|
import { getPhotos } from '@/photo/db/query';
|
||||||
@ -11,34 +8,36 @@ import { getDataForCategoriesCached } from '@/category/cache';
|
|||||||
import { getPhotosMetaCached } from '@/photo/cache';
|
import { getPhotosMetaCached } from '@/photo/cache';
|
||||||
import { SortProps } from '@/photo/db/sort';
|
import { SortProps } from '@/photo/db/sort';
|
||||||
import { getSortOptionsFromParams } from '@/photo/db/sort-path';
|
import { getSortOptionsFromParams } from '@/photo/db/sort-path';
|
||||||
|
import { FEED_META_QUERY_OPTIONS, getFeedQueryOptions } from '@/feed';
|
||||||
import { PhotoQueryOptions } from '@/photo/db';
|
import { PhotoQueryOptions } from '@/photo/db';
|
||||||
|
|
||||||
export const maxDuration = 60;
|
export const maxDuration = 60;
|
||||||
|
|
||||||
const getPhotosCached = cache((options: PhotoQueryOptions) => getPhotos({
|
const getPhotosCached = cache((options: PhotoQueryOptions) =>
|
||||||
|
getPhotos(getFeedQueryOptions({
|
||||||
|
isGrid: true,
|
||||||
...options,
|
...options,
|
||||||
limit: INFINITE_SCROLL_GRID_INITIAL,
|
})));
|
||||||
}));
|
|
||||||
|
|
||||||
export async function generateMetadata({
|
export async function generateMetadata({
|
||||||
params,
|
params,
|
||||||
}: SortProps): Promise<Metadata> {
|
}: SortProps): Promise<Metadata> {
|
||||||
const options = await getSortOptionsFromParams(params);
|
const sortOptions = await getSortOptionsFromParams(params);
|
||||||
const photos = await getPhotosCached(options)
|
const photos = await getPhotosCached(sortOptions)
|
||||||
.catch(() => []);
|
.catch(() => []);
|
||||||
return generateOgImageMetaForPhotos(photos);
|
return generateOgImageMetaForPhotos(photos);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function GridPage({ params }: SortProps) {
|
export default async function GridPage({ params }: SortProps) {
|
||||||
const options = await getSortOptionsFromParams(params);
|
const sortOptions = await getSortOptionsFromParams(params);
|
||||||
const [
|
const [
|
||||||
photos,
|
photos,
|
||||||
photosCount,
|
photosCount,
|
||||||
categories,
|
categories,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
getPhotosCached(options)
|
getPhotosCached(sortOptions)
|
||||||
.catch(() => []),
|
.catch(() => []),
|
||||||
getPhotosMetaCached(options)
|
getPhotosMetaCached(FEED_META_QUERY_OPTIONS)
|
||||||
.then(({ count }) => count)
|
.then(({ count }) => count)
|
||||||
.catch(() => 0),
|
.catch(() => 0),
|
||||||
getDataForCategoriesCached(),
|
getDataForCategoriesCached(),
|
||||||
@ -50,7 +49,7 @@ export default async function GridPage({ params }: SortProps) {
|
|||||||
{...{
|
{...{
|
||||||
photos,
|
photos,
|
||||||
photosCount,
|
photosCount,
|
||||||
...options,
|
...sortOptions,
|
||||||
...categories,
|
...categories,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,7 +1,4 @@
|
|||||||
import {
|
import { generateOgImageMetaForPhotos } from '@/photo';
|
||||||
INFINITE_SCROLL_GRID_INITIAL,
|
|
||||||
generateOgImageMetaForPhotos,
|
|
||||||
} from '@/photo';
|
|
||||||
import PhotosEmptyState from '@/photo/PhotosEmptyState';
|
import PhotosEmptyState from '@/photo/PhotosEmptyState';
|
||||||
import { Metadata } from 'next/types';
|
import { Metadata } from 'next/types';
|
||||||
import { getPhotos } from '@/photo/db/query';
|
import { getPhotos } from '@/photo/db/query';
|
||||||
@ -10,18 +7,17 @@ import PhotoGridPage from '@/photo/PhotoGridPage';
|
|||||||
import { getDataForCategoriesCached } from '@/category/cache';
|
import { getDataForCategoriesCached } from '@/category/cache';
|
||||||
import { getPhotosMetaCached } from '@/photo/cache';
|
import { getPhotosMetaCached } from '@/photo/cache';
|
||||||
import { USER_DEFAULT_SORT_OPTIONS } from '@/app/config';
|
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 dynamic = 'force-static';
|
||||||
export const maxDuration = 60;
|
export const maxDuration = 60;
|
||||||
|
|
||||||
const getPhotosCached = cache((options: PhotoQueryOptions) => getPhotos({
|
const getPhotosCached = cache(() => getPhotos(getFeedQueryOptions({
|
||||||
...options,
|
isGrid: true,
|
||||||
limit: INFINITE_SCROLL_GRID_INITIAL,
|
})));
|
||||||
}));
|
|
||||||
|
|
||||||
export async function generateMetadata(): Promise<Metadata> {
|
export async function generateMetadata(): Promise<Metadata> {
|
||||||
const photos = await getPhotosCached(USER_DEFAULT_SORT_OPTIONS)
|
const photos = await getPhotosCached()
|
||||||
.catch(() => []);
|
.catch(() => []);
|
||||||
return generateOgImageMetaForPhotos(photos);
|
return generateOgImageMetaForPhotos(photos);
|
||||||
}
|
}
|
||||||
@ -32,9 +28,9 @@ export default async function GridPage() {
|
|||||||
photosCount,
|
photosCount,
|
||||||
categories,
|
categories,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
getPhotosCached(USER_DEFAULT_SORT_OPTIONS)
|
getPhotosCached()
|
||||||
.catch(() => []),
|
.catch(() => []),
|
||||||
getPhotosMetaCached(USER_DEFAULT_SORT_OPTIONS)
|
getPhotosMetaCached(FEED_META_QUERY_OPTIONS)
|
||||||
.then(({ count }) => count)
|
.then(({ count }) => count)
|
||||||
.catch(() => 0),
|
.catch(() => 0),
|
||||||
getDataForCategoriesCached(),
|
getDataForCategoriesCached(),
|
||||||
|
|||||||
@ -30,6 +30,7 @@ import RecipeModal from '@/recipe/RecipeModal';
|
|||||||
import ThemeColors from '@/app/ThemeColors';
|
import ThemeColors from '@/app/ThemeColors';
|
||||||
import AppTextProvider from '@/i18n/state/AppTextProvider';
|
import AppTextProvider from '@/i18n/state/AppTextProvider';
|
||||||
import SharedHoverProvider from '@/components/shared-hover/SharedHoverProvider';
|
import SharedHoverProvider from '@/components/shared-hover/SharedHoverProvider';
|
||||||
|
import { PATH_FEED_JSON, PATH_RSS_XML } from '@/app/paths';
|
||||||
|
|
||||||
import '../tailwind.css';
|
import '../tailwind.css';
|
||||||
|
|
||||||
@ -71,7 +72,8 @@ export const metadata: Metadata = {
|
|||||||
...SITE_FEEDS_ENABLED && {
|
...SITE_FEEDS_ENABLED && {
|
||||||
alternates: {
|
alternates: {
|
||||||
types: {
|
types: {
|
||||||
'application/rss+xml': '/rss.xml',
|
'application/rss+xml': PATH_RSS_XML,
|
||||||
|
'application/json': PATH_FEED_JSON,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -18,7 +18,13 @@ import { staticallyGeneratePhotosIfConfigured } from '@/app/static';
|
|||||||
export const maxDuration = 60;
|
export const maxDuration = 60;
|
||||||
|
|
||||||
const getPhotosNearIdCachedCached = cache((photoId: string) =>
|
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(
|
export const generateStaticParams = staticallyGeneratePhotosIfConfigured(
|
||||||
'page',
|
'page',
|
||||||
|
|||||||
27
app/page.tsx
27
app/page.tsx
@ -1,32 +1,25 @@
|
|||||||
import {
|
import { generateOgImageMetaForPhotos } from '@/photo';
|
||||||
INFINITE_SCROLL_FEED_INITIAL,
|
|
||||||
INFINITE_SCROLL_GRID_INITIAL,
|
|
||||||
generateOgImageMetaForPhotos,
|
|
||||||
} from '@/photo';
|
|
||||||
import PhotosEmptyState from '@/photo/PhotosEmptyState';
|
import PhotosEmptyState from '@/photo/PhotosEmptyState';
|
||||||
import { Metadata } from 'next/types';
|
import { Metadata } from 'next/types';
|
||||||
import { cache } from 'react';
|
import { cache } from 'react';
|
||||||
import { getPhotos } from '@/photo/db/query';
|
import { getPhotos } from '@/photo/db/query';
|
||||||
import { GRID_HOMEPAGE_ENABLED, USER_DEFAULT_SORT_OPTIONS } from '@/app/config';
|
import { GRID_HOMEPAGE_ENABLED, USER_DEFAULT_SORT_OPTIONS } from '@/app/config';
|
||||||
import { NULL_CATEGORY_DATA } from '@/category/data';
|
import { NULL_CATEGORY_DATA } from '@/category/data';
|
||||||
import PhotoFeedPage from '@/photo/PhotoFeedPage';
|
import PhotoFullPage from '@/photo/PhotoFullPage';
|
||||||
import PhotoGridPage from '@/photo/PhotoGridPage';
|
import PhotoGridPage from '@/photo/PhotoGridPage';
|
||||||
import { getDataForCategoriesCached } from '@/category/cache';
|
import { getDataForCategoriesCached } from '@/category/cache';
|
||||||
import { getPhotosMetaCached } from '@/photo/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 dynamic = 'force-static';
|
||||||
export const maxDuration = 60;
|
export const maxDuration = 60;
|
||||||
|
|
||||||
const getPhotosCached = cache((options: PhotoQueryOptions) => getPhotos({
|
const getPhotosCached = cache(() => getPhotos(getFeedQueryOptions({
|
||||||
...options,
|
isGrid: GRID_HOMEPAGE_ENABLED,
|
||||||
limit: GRID_HOMEPAGE_ENABLED
|
})));
|
||||||
? INFINITE_SCROLL_GRID_INITIAL
|
|
||||||
: INFINITE_SCROLL_FEED_INITIAL,
|
|
||||||
}));
|
|
||||||
|
|
||||||
export async function generateMetadata(): Promise<Metadata> {
|
export async function generateMetadata(): Promise<Metadata> {
|
||||||
const photos = await getPhotosCached(USER_DEFAULT_SORT_OPTIONS)
|
const photos = await getPhotosCached()
|
||||||
.catch(() => []);
|
.catch(() => []);
|
||||||
return generateOgImageMetaForPhotos(photos);
|
return generateOgImageMetaForPhotos(photos);
|
||||||
}
|
}
|
||||||
@ -37,9 +30,9 @@ export default async function HomePage() {
|
|||||||
photosCount,
|
photosCount,
|
||||||
categories,
|
categories,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
getPhotosCached(USER_DEFAULT_SORT_OPTIONS)
|
getPhotosCached()
|
||||||
.catch(() => []),
|
.catch(() => []),
|
||||||
getPhotosMetaCached(USER_DEFAULT_SORT_OPTIONS)
|
getPhotosMetaCached(FEED_META_QUERY_OPTIONS)
|
||||||
.then(({ count }) => count)
|
.then(({ count }) => count)
|
||||||
.catch(() => 0),
|
.catch(() => 0),
|
||||||
GRID_HOMEPAGE_ENABLED
|
GRID_HOMEPAGE_ENABLED
|
||||||
@ -58,7 +51,7 @@ export default async function HomePage() {
|
|||||||
...categories,
|
...categories,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
: <PhotoFeedPage {...{
|
: <PhotoFullPage {...{
|
||||||
photos,
|
photos,
|
||||||
photosCount,
|
photosCount,
|
||||||
...USER_DEFAULT_SORT_OPTIONS,
|
...USER_DEFAULT_SORT_OPTIONS,
|
||||||
|
|||||||
@ -1,17 +1,15 @@
|
|||||||
import { getPhotosCached } from '@/photo/cache';
|
import { getPhotosCached } from '@/photo/cache';
|
||||||
import { SITE_FEEDS_ENABLED } from '@/app/config';
|
import { SITE_FEEDS_ENABLED } from '@/app/config';
|
||||||
import { FEED_PHOTO_REQUEST_LIMIT } from '@/feed';
|
|
||||||
import { formatFeedRssXml } from '@/feed/rss';
|
import { formatFeedRssXml } from '@/feed/rss';
|
||||||
|
import { PROGRAMMATIC_QUERY_OPTIONS } from '@/feed';
|
||||||
|
|
||||||
// Cache for 24 hours
|
// Cache for 24 hours
|
||||||
export const revalidate = 86_400;
|
export const revalidate = 86_400;
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
if (SITE_FEEDS_ENABLED) {
|
if (SITE_FEEDS_ENABLED) {
|
||||||
const photos = await getPhotosCached({
|
const photos = await getPhotosCached(PROGRAMMATIC_QUERY_OPTIONS)
|
||||||
limit: FEED_PHOTO_REQUEST_LIMIT,
|
.catch(() => []);
|
||||||
sortBy: 'createdAt',
|
|
||||||
}).catch(() => []);
|
|
||||||
return new Response(
|
return new Response(
|
||||||
formatFeedRssXml(photos),
|
formatFeedRssXml(photos),
|
||||||
{ headers: { 'Content-Type': 'text/xml' } },
|
{ headers: { 'Content-Type': 'text/xml' } },
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import type { MetadataRoute } from 'next';
|
import type { MetadataRoute } from 'next';
|
||||||
import { getDataForCategoriesCached } from '@/category/cache';
|
import { getDataForCategoriesCached } from '@/category/cache';
|
||||||
import {
|
import {
|
||||||
|
ABSOLUTE_PATH_FULL,
|
||||||
|
ABSOLUTE_PATH_GRID,
|
||||||
absolutePathForCamera,
|
absolutePathForCamera,
|
||||||
absolutePathForFilm,
|
absolutePathForFilm,
|
||||||
absolutePathForFocalLength,
|
absolutePathForFocalLength,
|
||||||
@ -72,9 +74,9 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|||||||
priority: PRIORITY_HOME,
|
priority: PRIORITY_HOME,
|
||||||
lastModified: lastModifiedSite,
|
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,
|
priority: PRIORITY_HOME_VIEW,
|
||||||
lastModified: lastModifiedSite,
|
lastModified: lastModifiedSite,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import {
|
|||||||
getPhotosNearIdCached,
|
getPhotosNearIdCached,
|
||||||
} from '@/photo/cache';
|
} from '@/photo/cache';
|
||||||
import { PATH_ROOT, absolutePathForPhoto } from '@/app/paths';
|
import { PATH_ROOT, absolutePathForPhoto } from '@/app/paths';
|
||||||
import { TAG_HIDDEN } from '@/tag';
|
import { TAG_PRIVATE } from '@/tag';
|
||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next';
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
import { cache } from 'react';
|
import { cache } from 'react';
|
||||||
@ -36,7 +36,7 @@ export async function generateMetadata({
|
|||||||
const title = titleForPhoto(photo);
|
const title = titleForPhoto(photo);
|
||||||
const description = descriptionForPhoto(photo);
|
const description = descriptionForPhoto(photo);
|
||||||
const descriptionHtml = descriptionForPhoto(photo, true);
|
const descriptionHtml = descriptionForPhoto(photo, true);
|
||||||
const url = absolutePathForPhoto({ photo, tag: TAG_HIDDEN });
|
const url = absolutePathForPhoto({ photo, tag: TAG_PRIVATE });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
@ -54,7 +54,7 @@ export async function generateMetadata({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function PhotoTagHiddenPage({
|
export default async function PhotoTagPrivatePage({
|
||||||
params,
|
params,
|
||||||
}: PhotoTagProps) {
|
}: PhotoTagProps) {
|
||||||
const { photoId } = await params;
|
const { photoId } = await params;
|
||||||
@ -74,7 +74,7 @@ export default async function PhotoTagHiddenPage({
|
|||||||
indexNumber,
|
indexNumber,
|
||||||
count,
|
count,
|
||||||
dateRange,
|
dateRange,
|
||||||
tag: TAG_HIDDEN,
|
tag: TAG_PRIVATE,
|
||||||
shouldShare: false,
|
shouldShare: false,
|
||||||
includeFavoriteInAdminMenu: false,
|
includeFavoriteInAdminMenu: false,
|
||||||
}} />
|
}} />
|
||||||
@ -4,8 +4,8 @@ import AppGrid from '@/components/AppGrid';
|
|||||||
import PhotoGrid from '@/photo/PhotoGrid';
|
import PhotoGrid from '@/photo/PhotoGrid';
|
||||||
import { getPhotosMetaCached, getPhotosNoStore } from '@/photo/cache';
|
import { getPhotosMetaCached, getPhotosNoStore } from '@/photo/cache';
|
||||||
import { absolutePathForTag } from '@/app/paths';
|
import { absolutePathForTag } from '@/app/paths';
|
||||||
import { TAG_HIDDEN, descriptionForTaggedPhotos, titleForTag } from '@/tag';
|
import { TAG_PRIVATE, descriptionForTaggedPhotos, titleForTag } from '@/tag';
|
||||||
import HiddenHeader from '@/tag/HiddenHeader';
|
import PrivateHeader from '@/tag/PrivateHeader';
|
||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next';
|
||||||
import { cache } from 'react';
|
import { cache } from 'react';
|
||||||
import { getAppText } from '@/i18n/state/server';
|
import { getAppText } from '@/i18n/state/server';
|
||||||
@ -20,7 +20,7 @@ export async function generateMetadata(): Promise<Metadata> {
|
|||||||
|
|
||||||
const appText = await getAppText();
|
const appText = await getAppText();
|
||||||
|
|
||||||
const title = titleForTag(TAG_HIDDEN, undefined, appText, count);
|
const title = titleForTag(TAG_PRIVATE, undefined, appText, count);
|
||||||
|
|
||||||
const description = descriptionForTaggedPhotos(
|
const description = descriptionForTaggedPhotos(
|
||||||
undefined,
|
undefined,
|
||||||
@ -29,7 +29,7 @@ export async function generateMetadata(): Promise<Metadata> {
|
|||||||
count,
|
count,
|
||||||
dateRange,
|
dateRange,
|
||||||
);
|
);
|
||||||
const url = absolutePathForTag(TAG_HIDDEN);
|
const url = absolutePathForTag(TAG_PRIVATE);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
@ -46,7 +46,7 @@ export async function generateMetadata(): Promise<Metadata> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function HiddenTagPage() {
|
export default async function PrivateTagPage() {
|
||||||
const [
|
const [
|
||||||
photos,
|
photos,
|
||||||
{ count, dateRange },
|
{ count, dateRange },
|
||||||
@ -60,15 +60,15 @@ export default async function HiddenTagPage() {
|
|||||||
contentMain={<div className="space-y-4 mt-4">
|
contentMain={<div className="space-y-4 mt-4">
|
||||||
<AnimateItems
|
<AnimateItems
|
||||||
type="bottom"
|
type="bottom"
|
||||||
items={[<HiddenHeader
|
items={[<PrivateHeader
|
||||||
key="HiddenHeader"
|
key="PrivateHeader"
|
||||||
{...{ photos, count, dateRange }}
|
{...{ photos, count, dateRange }}
|
||||||
/>]}
|
/>]}
|
||||||
animateOnFirstLoadOnly
|
animateOnFirstLoadOnly
|
||||||
/>
|
/>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<Note animate>
|
<Note animate>
|
||||||
Only visible to authenticated admins
|
Visible only to admins (uploads only secure via obscurity)
|
||||||
</Note>
|
</Note>
|
||||||
<PhotoGrid {...{ photos }} />
|
<PhotoGrid {...{ photos }} />
|
||||||
</div>
|
</div>
|
||||||
@ -46,12 +46,12 @@ export const config = {
|
|||||||
// - /_next/image*
|
// - /_next/image*
|
||||||
// - /favicon.ico + /favicons/*
|
// - /favicon.ico + /favicons/*
|
||||||
// - /grid
|
// - /grid
|
||||||
// - /feed
|
// - /full
|
||||||
// - / (root)
|
// - / (root)
|
||||||
// - /home-image
|
// - /home-image
|
||||||
// - /template-image
|
// - /template-image
|
||||||
// - /template-image-tight
|
// - /template-image-tight
|
||||||
// - /template-url
|
// - /template-url
|
||||||
// eslint-disable-next-line max-len
|
// 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$|$).*)'],
|
||||||
};
|
};
|
||||||
|
|||||||
@ -635,7 +635,7 @@ export default function AdminAppConfigurationClient({
|
|||||||
)}
|
)}
|
||||||
</Fragment>)}
|
</Fragment>)}
|
||||||
</div>
|
</div>
|
||||||
Change default sort on grid/feed homepages
|
Change default sort on grid/full homepages
|
||||||
{renderEnvVars(['NEXT_PUBLIC_DEFAULT_SORT'])}
|
{renderEnvVars(['NEXT_PUBLIC_DEFAULT_SORT'])}
|
||||||
</ChecklistRow>
|
</ChecklistRow>
|
||||||
<ChecklistRow
|
<ChecklistRow
|
||||||
@ -654,7 +654,7 @@ export default function AdminAppConfigurationClient({
|
|||||||
optional
|
optional
|
||||||
>
|
>
|
||||||
Set environment variable to {'"1"'} to
|
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'])}
|
{renderEnvVars(['NEXT_PUBLIC_SHOW_SORT_CONTROL'])}
|
||||||
</ChecklistRow>
|
</ChecklistRow>
|
||||||
</ChecklistGroup>
|
</ChecklistGroup>
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import {
|
|||||||
import sleep from '@/utility/sleep';
|
import sleep from '@/utility/sleep';
|
||||||
import { readStreamableValue } from 'ai/rsc';
|
import { readStreamableValue } from 'ai/rsc';
|
||||||
import { useRouter } from 'next/navigation';
|
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 { BiCheckCircle } from 'react-icons/bi';
|
||||||
import ProgressButton from '@/components/primitives/ProgressButton';
|
import ProgressButton from '@/components/primitives/ProgressButton';
|
||||||
import { UrlAddStatus } from './AdminUploadsClient';
|
import { UrlAddStatus } from './AdminUploadsClient';
|
||||||
@ -22,8 +22,9 @@ import DeleteUploadButton from './DeleteUploadButton';
|
|||||||
import { useAppState } from '@/app/AppState';
|
import { useAppState } from '@/app/AppState';
|
||||||
import { pluralize } from '@/utility/string';
|
import { pluralize } from '@/utility/string';
|
||||||
import FieldsetFavs from '@/photo/form/FieldsetFavs';
|
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 IconAddUpload from '@/components/icons/IconAddUpload';
|
||||||
|
import FieldsetExclude from '@/photo/form/FieldsetExclude';
|
||||||
|
|
||||||
const UPLOAD_BATCH_SIZE = 2;
|
const UPLOAD_BATCH_SIZE = 2;
|
||||||
|
|
||||||
@ -53,6 +54,7 @@ export default function AdminBatchUploadActions({
|
|||||||
const [showBulkSettings, setShowBulkSettings] = useState(false);
|
const [showBulkSettings, setShowBulkSettings] = useState(false);
|
||||||
const [tags, setTags] = useState('');
|
const [tags, setTags] = useState('');
|
||||||
const [favorite, setFavorite] = useState('false');
|
const [favorite, setFavorite] = useState('false');
|
||||||
|
const [excludeFromFeeds, setExcludeFromFeeds] = useState('false');
|
||||||
const [hidden, setHidden] = useState('false');
|
const [hidden, setHidden] = useState('false');
|
||||||
const [tagErrorMessage, setTagErrorMessage] = useState('');
|
const [tagErrorMessage, setTagErrorMessage] = useState('');
|
||||||
|
|
||||||
@ -76,6 +78,7 @@ export default function AdminBatchUploadActions({
|
|||||||
...showBulkSettings && {
|
...showBulkSettings && {
|
||||||
tags,
|
tags,
|
||||||
favorite,
|
favorite,
|
||||||
|
excludeFromFeeds,
|
||||||
hidden,
|
hidden,
|
||||||
},
|
},
|
||||||
takenAtLocal: generateLocalPostgresString(),
|
takenAtLocal: generateLocalPostgresString(),
|
||||||
@ -123,12 +126,19 @@ export default function AdminBatchUploadActions({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (hidden === 'true') {
|
||||||
|
setFavorite('false');
|
||||||
|
setExcludeFromFeeds('false');
|
||||||
|
}
|
||||||
|
}, [hidden]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{actionErrorMessage &&
|
{actionErrorMessage &&
|
||||||
<ErrorNote>{actionErrorMessage}</ErrorNote>}
|
<ErrorNote>{actionErrorMessage}</ErrorNote>}
|
||||||
<Container padding="tight">
|
<Container padding="tight" className="p-2! sm:p-3!">
|
||||||
<div className="w-full space-y-4 py-1">
|
<div className="w-full space-y-4">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div className="grow text-main">
|
<div className="grow text-main">
|
||||||
{showBulkSettings
|
{showBulkSettings
|
||||||
@ -154,20 +164,25 @@ export default function AdminBatchUploadActions({
|
|||||||
readOnly={isAdding}
|
readOnly={isAdding}
|
||||||
className="relative z-10"
|
className="relative z-10"
|
||||||
/>
|
/>
|
||||||
<div className="flex gap-8">
|
<div className="flex max-sm:flex-col gap-x-8 gap-y-4">
|
||||||
<FieldsetFavs
|
<FieldsetFavs
|
||||||
value={favorite}
|
value={favorite}
|
||||||
onChange={setFavorite}
|
onChange={setFavorite}
|
||||||
readOnly={isAdding}
|
readOnly={isAdding || hidden === 'true'}
|
||||||
/>
|
/>
|
||||||
<FieldsetHidden
|
<FieldsetExclude
|
||||||
|
value={excludeFromFeeds}
|
||||||
|
onChange={setExcludeFromFeeds}
|
||||||
|
readOnly={isAdding || hidden === 'true'}
|
||||||
|
/>
|
||||||
|
<FieldsetPrivate
|
||||||
value={hidden}
|
value={hidden}
|
||||||
onChange={setHidden}
|
onChange={setHidden}
|
||||||
readOnly={isAdding}
|
readOnly={isAdding}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>}
|
</div>}
|
||||||
<div className="space-y-2">
|
<div className="flex flex-col sm:flex-row-reverse gap-2">
|
||||||
<ProgressButton
|
<ProgressButton
|
||||||
primary
|
primary
|
||||||
className="w-full justify-center"
|
className="w-full justify-center"
|
||||||
|
|||||||
@ -11,14 +11,14 @@ import {
|
|||||||
deletePhotoAction,
|
deletePhotoAction,
|
||||||
syncPhotoAction,
|
syncPhotoAction,
|
||||||
toggleFavoritePhotoAction,
|
toggleFavoritePhotoAction,
|
||||||
toggleHidePhotoAction,
|
togglePrivatePhotoAction,
|
||||||
} from '@/photo/actions';
|
} from '@/photo/actions';
|
||||||
import {
|
import {
|
||||||
Photo,
|
Photo,
|
||||||
deleteConfirmationTextForPhoto,
|
deleteConfirmationTextForPhoto,
|
||||||
downloadFileNameForPhoto,
|
downloadFileNameForPhoto,
|
||||||
} from '@/photo';
|
} from '@/photo';
|
||||||
import { isPathFavs, isPhotoFav, TAG_HIDDEN } from '@/tag';
|
import { isPathFavs, isPhotoFav, TAG_PRIVATE } from '@/tag';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import { BiTrash } from 'react-icons/bi';
|
import { BiTrash } from 'react-icons/bi';
|
||||||
import MoreMenu from '@/components/more/MoreMenu';
|
import MoreMenu from '@/components/more/MoreMenu';
|
||||||
@ -33,7 +33,7 @@ import IconEdit from '@/components/icons/IconEdit';
|
|||||||
import { photoNeedsToBeSynced } from '@/photo/sync';
|
import { photoNeedsToBeSynced } from '@/photo/sync';
|
||||||
import { KEY_COMMANDS } from '@/photo/key-commands';
|
import { KEY_COMMANDS } from '@/photo/key-commands';
|
||||||
import { useAppText } from '@/i18n/state/client';
|
import { useAppText } from '@/i18n/state/client';
|
||||||
import IconHidden from '@/components/icons/IconHidden';
|
import IconLock from '@/components/icons/IconLock';
|
||||||
|
|
||||||
export default function AdminPhotoMenu({
|
export default function AdminPhotoMenu({
|
||||||
photo,
|
photo,
|
||||||
@ -57,9 +57,9 @@ export default function AdminPhotoMenu({
|
|||||||
const isFav = isPhotoFav(photo);
|
const isFav = isPhotoFav(photo);
|
||||||
const shouldRedirectFav = isPathFavs(path) && isFav;
|
const shouldRedirectFav = isPathFavs(path) && isFav;
|
||||||
const shouldRedirectDelete = isOnPhotoDetail;
|
const shouldRedirectDelete = isOnPhotoDetail;
|
||||||
const redirectPathOnHideToggle = isOnPhotoDetail
|
const redirectPathOnPrivateToggle = isOnPhotoDetail
|
||||||
? photo.hidden
|
? photo.hidden
|
||||||
? pathForTag(TAG_HIDDEN)
|
? pathForTag(TAG_PRIVATE)
|
||||||
: PATH_ROOT
|
: PATH_ROOT
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
@ -68,8 +68,8 @@ export default function AdminPhotoMenu({
|
|||||||
const items: ComponentProps<typeof MoreMenuItem>[] = [{
|
const items: ComponentProps<typeof MoreMenuItem>[] = [{
|
||||||
label: appText.admin.edit,
|
label: appText.admin.edit,
|
||||||
icon: <IconEdit
|
icon: <IconEdit
|
||||||
size={15}
|
size={14}
|
||||||
className="translate-x-[0.5px]"
|
className="translate-x-[0.5px] translate-y-[0.5px]"
|
||||||
/>,
|
/>,
|
||||||
href: pathForAdminPhotoEdit(photo.id),
|
href: pathForAdminPhotoEdit(photo.id),
|
||||||
...showKeyCommands && { keyCommand: KEY_COMMANDS.edit },
|
...showKeyCommands && { keyCommand: KEY_COMMANDS.edit },
|
||||||
@ -94,19 +94,20 @@ export default function AdminPhotoMenu({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
items.push({
|
items.push({
|
||||||
label: photo.hidden ? appText.admin.unhide : appText.admin.hide,
|
label: photo.hidden ? appText.admin.public : appText.admin.private,
|
||||||
icon: <IconHidden
|
icon: <IconLock
|
||||||
size={17}
|
size={16}
|
||||||
className="translate-x-[-1px] translate-y-[1px]"
|
className="translate-x-[-1.5px] translate-y-[0.5px]"
|
||||||
visible={photo.hidden}
|
open={!photo.hidden}
|
||||||
|
narrow
|
||||||
/>,
|
/>,
|
||||||
action: () => toggleHidePhotoAction(
|
action: () => togglePrivatePhotoAction(
|
||||||
photo.id,
|
photo.id,
|
||||||
redirectPathOnHideToggle,
|
redirectPathOnPrivateToggle,
|
||||||
)
|
)
|
||||||
.then(() => revalidatePhoto?.(photo.id)),
|
.then(() => revalidatePhoto?.(photo.id)),
|
||||||
...showKeyCommands && {
|
...showKeyCommands && {
|
||||||
keyCommand: KEY_COMMANDS.toggleHide,
|
keyCommand: KEY_COMMANDS.togglePrivate,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
items.push({
|
items.push({
|
||||||
@ -146,7 +147,7 @@ export default function AdminPhotoMenu({
|
|||||||
includeFavorite,
|
includeFavorite,
|
||||||
isFav,
|
isFav,
|
||||||
shouldRedirectFav,
|
shouldRedirectFav,
|
||||||
redirectPathOnHideToggle,
|
redirectPathOnPrivateToggle,
|
||||||
revalidatePhoto,
|
revalidatePhoto,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import { Timezone } from '@/utility/timezone';
|
|||||||
import IconHidden from '@/components/icons/IconHidden';
|
import IconHidden from '@/components/icons/IconHidden';
|
||||||
import Tooltip from '@/components/Tooltip';
|
import Tooltip from '@/components/Tooltip';
|
||||||
import { photoNeedsToBeSynced, getPhotoSyncStatusText } from '@/photo/sync';
|
import { photoNeedsToBeSynced, getPhotoSyncStatusText } from '@/photo/sync';
|
||||||
|
import IconLock from '@/components/icons/IconLock';
|
||||||
|
|
||||||
export default function AdminPhotosTable({
|
export default function AdminPhotosTable({
|
||||||
photos,
|
photos,
|
||||||
@ -76,13 +77,20 @@ export default function AdminPhotosTable({
|
|||||||
<span className="truncate">
|
<span className="truncate">
|
||||||
{titleForPhoto(photo, false)}
|
{titleForPhoto(photo, false)}
|
||||||
</span>
|
</span>
|
||||||
{photo.hidden &&
|
{photo.excludeFromFeeds && !photo.hidden &&
|
||||||
<span>
|
<span>
|
||||||
<IconHidden
|
<IconHidden
|
||||||
className="inline translate-y-[-0.5px]"
|
className="inline translate-y-[-1px]"
|
||||||
size={16}
|
size={16}
|
||||||
/>
|
/>
|
||||||
</span>}
|
</span>}
|
||||||
|
{photo.hidden &&
|
||||||
|
<span>
|
||||||
|
<IconLock
|
||||||
|
size={13}
|
||||||
|
className="inline translate-y-[-1.5px]"
|
||||||
|
/>
|
||||||
|
</span>}
|
||||||
</span>
|
</span>
|
||||||
{photo.priorityOrder !== null &&
|
{photo.priorityOrder !== null &&
|
||||||
<span className={clsx(
|
<span className={clsx(
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import PhotoTag from '@/tag/PhotoTag';
|
import PhotoTag from '@/tag/PhotoTag';
|
||||||
import { photoLabelForCount } from '@/photo';
|
import { photoLabelForCount } from '@/photo';
|
||||||
import { clsx } from 'clsx/lite';
|
import { clsx } from 'clsx/lite';
|
||||||
import FavsTag from '@/tag/FavsTag';
|
import PhotoFavs from '@/tag/PhotoFavs';
|
||||||
import { isTagFavs } from '@/tag';
|
import { isTagFavs } from '@/tag';
|
||||||
import Badge from '@/components/Badge';
|
import Badge from '@/components/Badge';
|
||||||
import { getAppText } from '@/i18n/state/server';
|
import { getAppText } from '@/i18n/state/server';
|
||||||
@ -25,7 +25,7 @@ export default async function AdminTagBadge({
|
|||||||
isTagFavs(tag) && 'translate-y-[0.5px]',
|
isTagFavs(tag) && 'translate-y-[0.5px]',
|
||||||
)}>
|
)}>
|
||||||
{isTagFavs(tag)
|
{isTagFavs(tag)
|
||||||
? <FavsTag />
|
? <PhotoFavs />
|
||||||
: <PhotoTag {...{ tag }} />}
|
: <PhotoTag {...{ tag }} />}
|
||||||
<div className="text-dim uppercase">
|
<div className="text-dim uppercase">
|
||||||
<span>{count}</span>
|
<span>{count}</span>
|
||||||
|
|||||||
@ -75,7 +75,7 @@ export default function AdminUploadsTableRow({
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
'flex items-center grow',
|
'flex items-center grow',
|
||||||
'transition-opacity',
|
'transition-opacity',
|
||||||
'rounded-md overflow-hidden',
|
'rounded-lg overflow-hidden',
|
||||||
'border-medium bg-extra-dim',
|
'border-medium bg-extra-dim',
|
||||||
isAdding && !isComplete && status !== 'adding' && 'opacity-30',
|
isAdding && !isComplete && status !== 'adding' && 'opacity-30',
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import Switcher from '@/components/Switcher';
|
import Switcher from '@/components/Switcher';
|
||||||
import SwitcherItem from '@/components/SwitcherItem';
|
import SwitcherItem from '@/components/SwitcherItem';
|
||||||
import IconFeed from '@/components/icons/IconFeed';
|
import IconFull from '@/components/icons/IconFull';
|
||||||
import IconGrid from '@/components/icons/IconGrid';
|
import IconGrid from '@/components/icons/IconGrid';
|
||||||
import {
|
import {
|
||||||
doesPathOfferSort,
|
doesPathOfferSort,
|
||||||
PATH_FEED_INFERRED,
|
PATH_FULL_INFERRED,
|
||||||
PATH_GRID_INFERRED,
|
PATH_GRID_INFERRED,
|
||||||
} from '@/app/paths';
|
} from '@/app/paths';
|
||||||
import IconSearch from '../components/icons/IconSearch';
|
import IconSearch from '../components/icons/IconSearch';
|
||||||
@ -26,7 +26,7 @@ import IconSort from '@/components/icons/IconSort';
|
|||||||
import { getSortConfigFromPath } from '@/photo/db/sort-path';
|
import { getSortConfigFromPath } from '@/photo/db/sort-path';
|
||||||
import { motion } from 'framer-motion';
|
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';
|
const GAP_CLASS = 'mr-1.5 sm:mr-2';
|
||||||
|
|
||||||
@ -55,7 +55,7 @@ export default function AppViewSwitcher({
|
|||||||
sortBy,
|
sortBy,
|
||||||
isAscending,
|
isAscending,
|
||||||
pathGrid,
|
pathGrid,
|
||||||
pathFeed,
|
pathFull,
|
||||||
pathSort,
|
pathSort,
|
||||||
} = getSortConfigFromPath(pathname);
|
} = getSortConfigFromPath(pathname);
|
||||||
|
|
||||||
@ -68,14 +68,14 @@ export default function AppViewSwitcher({
|
|||||||
hasLoadedRef.current = true;
|
hasLoadedRef.current = true;
|
||||||
}, [invalidateSwr, sortBy]);
|
}, [invalidateSwr, sortBy]);
|
||||||
|
|
||||||
const refHrefFeed = useRef<HTMLAnchorElement>(null);
|
const refHrefFull = useRef<HTMLAnchorElement>(null);
|
||||||
const refHrefGrid = useRef<HTMLAnchorElement>(null);
|
const refHrefGrid = useRef<HTMLAnchorElement>(null);
|
||||||
|
|
||||||
const onKeyDown = useCallback((e: KeyboardEvent) => {
|
const onKeyDown = useCallback((e: KeyboardEvent) => {
|
||||||
if (!e.metaKey) {
|
if (!e.metaKey) {
|
||||||
switch (e.key.toLocaleUpperCase()) {
|
switch (e.key.toLocaleUpperCase()) {
|
||||||
case KEY_COMMANDS.feed:
|
case KEY_COMMANDS.full:
|
||||||
if (pathname !== PATH_FEED_INFERRED) { refHrefFeed.current?.click(); }
|
if (pathname !== PATH_FULL_INFERRED) { refHrefFull.current?.click(); }
|
||||||
break;
|
break;
|
||||||
case KEY_COMMANDS.grid:
|
case KEY_COMMANDS.grid:
|
||||||
if (pathname !== PATH_GRID_INFERRED) { refHrefGrid.current?.click(); }
|
if (pathname !== PATH_GRID_INFERRED) { refHrefGrid.current?.click(); }
|
||||||
@ -90,15 +90,15 @@ export default function AppViewSwitcher({
|
|||||||
|
|
||||||
const [isAdminMenuOpen, setIsAdminMenuOpen] = useState(false);
|
const [isAdminMenuOpen, setIsAdminMenuOpen] = useState(false);
|
||||||
|
|
||||||
const renderItemFeed =
|
const renderItemFull =
|
||||||
<SwitcherItem
|
<SwitcherItem
|
||||||
icon={<IconFeed includeTitle={false} />}
|
icon={<IconFull includeTitle={false} />}
|
||||||
href={pathFeed}
|
href={pathFull}
|
||||||
hrefRef={refHrefFeed}
|
hrefRef={refHrefFull}
|
||||||
active={currentSelection === 'feed'}
|
active={currentSelection === 'full'}
|
||||||
tooltip={{...SHOW_KEYBOARD_SHORTCUT_TOOLTIPS && {
|
tooltip={{...SHOW_KEYBOARD_SHORTCUT_TOOLTIPS && {
|
||||||
content: appText.nav.feed,
|
content: appText.nav.full,
|
||||||
keyCommand: KEY_COMMANDS.feed,
|
keyCommand: KEY_COMMANDS.full,
|
||||||
}}}
|
}}}
|
||||||
noPadding
|
noPadding
|
||||||
/>;
|
/>;
|
||||||
@ -119,8 +119,8 @@ export default function AppViewSwitcher({
|
|||||||
return (
|
return (
|
||||||
<div className={clsx('flex', className)}>
|
<div className={clsx('flex', className)}>
|
||||||
<Switcher className={GAP_CLASS}>
|
<Switcher className={GAP_CLASS}>
|
||||||
{GRID_HOMEPAGE_ENABLED ? renderItemGrid : renderItemFeed}
|
{GRID_HOMEPAGE_ENABLED ? renderItemGrid : renderItemFull}
|
||||||
{GRID_HOMEPAGE_ENABLED ? renderItemFeed : renderItemGrid}
|
{GRID_HOMEPAGE_ENABLED ? renderItemFull : renderItemGrid}
|
||||||
{/* Show spinner if admin is suspected to be logged in */}
|
{/* Show spinner if admin is suspected to be logged in */}
|
||||||
{(isUserSignedInEager && !isUserSignedIn) &&
|
{(isUserSignedInEager && !isUserSignedIn) &&
|
||||||
<SwitcherItem
|
<SwitcherItem
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import AppViewSwitcher, { SwitcherSelection } from '@/app/AppViewSwitcher';
|
|||||||
import {
|
import {
|
||||||
PATH_ROOT,
|
PATH_ROOT,
|
||||||
isPathAdmin,
|
isPathAdmin,
|
||||||
isPathFeed,
|
isPathFull,
|
||||||
isPathGrid,
|
isPathGrid,
|
||||||
isPathProtected,
|
isPathProtected,
|
||||||
isPathSignIn,
|
isPathSignIn,
|
||||||
@ -58,11 +58,11 @@ export default function Nav({
|
|||||||
|
|
||||||
const switcherSelectionForPath = (): SwitcherSelection | undefined => {
|
const switcherSelectionForPath = (): SwitcherSelection | undefined => {
|
||||||
if (pathname === PATH_ROOT) {
|
if (pathname === PATH_ROOT) {
|
||||||
return GRID_HOMEPAGE_ENABLED ? 'grid' : 'feed';
|
return GRID_HOMEPAGE_ENABLED ? 'grid' : 'full';
|
||||||
} else if (isPathGrid(pathname)) {
|
} else if (isPathGrid(pathname)) {
|
||||||
return 'grid';
|
return 'grid';
|
||||||
} else if (isPathFeed(pathname)) {
|
} else if (isPathFull(pathname)) {
|
||||||
return 'feed';
|
return 'full';
|
||||||
} else if (isPathProtected(pathname)) {
|
} else if (isPathProtected(pathname)) {
|
||||||
return 'admin';
|
return 'admin';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,13 +3,13 @@ import { PhotoSetCategory } from '@/category';
|
|||||||
import { getBaseUrl, GRID_HOMEPAGE_ENABLED } from './config';
|
import { getBaseUrl, GRID_HOMEPAGE_ENABLED } from './config';
|
||||||
import { Camera } from '@/camera';
|
import { Camera } from '@/camera';
|
||||||
import { parameterize } from '@/utility/string';
|
import { parameterize } from '@/utility/string';
|
||||||
import { TAG_HIDDEN } from '@/tag';
|
import { TAG_PRIVATE } from '@/tag';
|
||||||
import { Lens } from '@/lens';
|
import { Lens } from '@/lens';
|
||||||
|
|
||||||
// Core
|
// Core
|
||||||
export const PATH_ROOT = '/';
|
export const PATH_ROOT = '/';
|
||||||
export const PATH_GRID = '/grid';
|
export const PATH_GRID = '/grid';
|
||||||
export const PATH_FEED = '/feed';
|
export const PATH_FULL = '/full';
|
||||||
export const PATH_ADMIN = '/admin';
|
export const PATH_ADMIN = '/admin';
|
||||||
export const PATH_API = '/api';
|
export const PATH_API = '/api';
|
||||||
export const PATH_SIGN_IN = '/sign-in';
|
export const PATH_SIGN_IN = '/sign-in';
|
||||||
@ -19,8 +19,8 @@ export const PATH_OG = '/og';
|
|||||||
export const PATH_GRID_INFERRED = GRID_HOMEPAGE_ENABLED
|
export const PATH_GRID_INFERRED = GRID_HOMEPAGE_ENABLED
|
||||||
? PATH_ROOT
|
? PATH_ROOT
|
||||||
: PATH_GRID;
|
: PATH_GRID;
|
||||||
export const PATH_FEED_INFERRED = GRID_HOMEPAGE_ENABLED
|
export const PATH_FULL_INFERRED = GRID_HOMEPAGE_ENABLED
|
||||||
? PATH_FEED
|
? PATH_FULL
|
||||||
: PATH_ROOT;
|
: PATH_ROOT;
|
||||||
|
|
||||||
// Sort
|
// Sort
|
||||||
@ -31,7 +31,7 @@ export const PARAM_SORT_ORDER_OLDEST = 'oldest-first';
|
|||||||
export const doesPathOfferSort = (pathname: string) =>
|
export const doesPathOfferSort = (pathname: string) =>
|
||||||
pathname === PATH_ROOT ||
|
pathname === PATH_ROOT ||
|
||||||
pathname.startsWith(PATH_GRID) ||
|
pathname.startsWith(PATH_GRID) ||
|
||||||
pathname.startsWith(PATH_FEED);
|
pathname.startsWith(PATH_FULL);
|
||||||
|
|
||||||
// Feeds
|
// Feeds
|
||||||
export const PATH_SITEMAP = '/sitemap.xml';
|
export const PATH_SITEMAP = '/sitemap.xml';
|
||||||
@ -104,7 +104,7 @@ export const PATHS_ADMIN = [
|
|||||||
export const PATHS_TO_CACHE = [
|
export const PATHS_TO_CACHE = [
|
||||||
PATH_ROOT,
|
PATH_ROOT,
|
||||||
PATH_GRID,
|
PATH_GRID,
|
||||||
PATH_FEED,
|
PATH_FULL,
|
||||||
PATH_OG,
|
PATH_OG,
|
||||||
PATH_PHOTO_DYNAMIC,
|
PATH_PHOTO_DYNAMIC,
|
||||||
PATH_CAMERA_DYNAMIC,
|
PATH_CAMERA_DYNAMIC,
|
||||||
@ -154,7 +154,7 @@ export const pathForPhoto = ({
|
|||||||
let prefix = PREFIX_PHOTO;
|
let prefix = PREFIX_PHOTO;
|
||||||
|
|
||||||
if (typeof photo !== 'string' && photo.hidden) {
|
if (typeof photo !== 'string' && photo.hidden) {
|
||||||
prefix = pathForTag(TAG_HIDDEN);
|
prefix = pathForTag(TAG_PRIVATE);
|
||||||
} else if (recent) {
|
} else if (recent) {
|
||||||
prefix = PREFIX_RECENTS;
|
prefix = PREFIX_RECENTS;
|
||||||
} else if (year) {
|
} else if (year) {
|
||||||
@ -231,13 +231,19 @@ export const pathForRecentsImage = () =>
|
|||||||
pathForImage(PREFIX_RECENTS);
|
pathForImage(PREFIX_RECENTS);
|
||||||
|
|
||||||
// Absolute paths
|
// 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}`;
|
`${getBaseUrl()}${PATH_FEED_JSON}`;
|
||||||
|
|
||||||
export const ABSOLUTE_PATH_FOR_RSS_XML =
|
export const ABSOLUTE_PATH_RSS_XML =
|
||||||
`${getBaseUrl()}${PATH_RSS_XML}`;
|
`${getBaseUrl()}${PATH_RSS_XML}`;
|
||||||
|
|
||||||
export const ABSOLUTE_PATH_FOR_HOME_IMAGE =
|
export const ABSOLUTE_PATH_HOME_IMAGE =
|
||||||
`${getBaseUrl()}/home-image`;
|
`${getBaseUrl()}/home-image`;
|
||||||
|
|
||||||
export const absolutePathForPhoto = (
|
export const absolutePathForPhoto = (
|
||||||
@ -374,13 +380,13 @@ export const isPathRoot = (pathname?: string) =>
|
|||||||
export const isPathGrid = (pathname?: string) =>
|
export const isPathGrid = (pathname?: string) =>
|
||||||
checkPathPrefix(pathname, PATH_GRID);
|
checkPathPrefix(pathname, PATH_GRID);
|
||||||
|
|
||||||
export const isPathFeed = (pathname?: string) =>
|
export const isPathFull = (pathname?: string) =>
|
||||||
checkPathPrefix(pathname, PATH_FEED);
|
checkPathPrefix(pathname, PATH_FULL);
|
||||||
|
|
||||||
export const isPathTopLevel = (pathname?: string) =>
|
export const isPathTopLevel = (pathname?: string) =>
|
||||||
isPathRoot(pathname)||
|
isPathRoot(pathname)||
|
||||||
isPathGrid(pathname) ||
|
isPathGrid(pathname) ||
|
||||||
isPathFeed(pathname);
|
isPathFull(pathname);
|
||||||
|
|
||||||
export const isPathSignIn = (pathname?: string) =>
|
export const isPathSignIn = (pathname?: string) =>
|
||||||
checkPathPrefix(pathname, PATH_SIGN_IN);
|
checkPathPrefix(pathname, PATH_SIGN_IN);
|
||||||
@ -406,7 +412,7 @@ export const isPathAdminInfo = (pathname?: string) =>
|
|||||||
|
|
||||||
export const isPathProtected = (pathname?: string) =>
|
export const isPathProtected = (pathname?: string) =>
|
||||||
checkPathPrefix(pathname, PATH_ADMIN) ||
|
checkPathPrefix(pathname, PATH_ADMIN) ||
|
||||||
checkPathPrefix(pathname, pathForTag(TAG_HIDDEN)) ||
|
checkPathPrefix(pathname, pathForTag(TAG_PRIVATE)) ||
|
||||||
checkPathPrefix(pathname, PATH_OG);
|
checkPathPrefix(pathname, PATH_OG);
|
||||||
|
|
||||||
export const getPathComponents = (pathname = ''): {
|
export const getPathComponents = (pathname = ''): {
|
||||||
|
|||||||
@ -20,7 +20,7 @@ import {
|
|||||||
PATH_ADMIN_RECIPES,
|
PATH_ADMIN_RECIPES,
|
||||||
PATH_ADMIN_TAGS,
|
PATH_ADMIN_TAGS,
|
||||||
PATH_ADMIN_UPLOADS,
|
PATH_ADMIN_UPLOADS,
|
||||||
PATH_FEED_INFERRED,
|
PATH_FULL_INFERRED,
|
||||||
PATH_GRID_INFERRED,
|
PATH_GRID_INFERRED,
|
||||||
PATH_SIGN_IN,
|
PATH_SIGN_IN,
|
||||||
pathForCamera,
|
pathForCamera,
|
||||||
@ -50,10 +50,10 @@ import PhotoDate from '@/photo/PhotoDate';
|
|||||||
import PhotoSmall from '@/photo/PhotoSmall';
|
import PhotoSmall from '@/photo/PhotoSmall';
|
||||||
import { FaCheck } from 'react-icons/fa6';
|
import { FaCheck } from 'react-icons/fa6';
|
||||||
import {
|
import {
|
||||||
addHiddenToTags,
|
addPrivateToTags,
|
||||||
formatTag,
|
formatTag,
|
||||||
isTagFavs,
|
isTagFavs,
|
||||||
isTagHidden,
|
isTagPrivate,
|
||||||
limitTagsByCount,
|
limitTagsByCount,
|
||||||
} from '@/tag';
|
} from '@/tag';
|
||||||
import { formatCount, formatCountDescriptive } from '@/utility/string';
|
import { formatCount, formatCountDescriptive } from '@/utility/string';
|
||||||
@ -84,7 +84,6 @@ import useVisualViewportHeight from '@/utility/useVisualViewport';
|
|||||||
import useMaskedScroll from '../components/useMaskedScroll';
|
import useMaskedScroll from '../components/useMaskedScroll';
|
||||||
import { labelForFilm } from '@/film';
|
import { labelForFilm } from '@/film';
|
||||||
import IconFavs from '@/components/icons/IconFavs';
|
import IconFavs from '@/components/icons/IconFavs';
|
||||||
import IconHidden from '@/components/icons/IconHidden';
|
|
||||||
import { useAppText } from '@/i18n/state/client';
|
import { useAppText } from '@/i18n/state/client';
|
||||||
import LoaderButton from '@/components/primitives/LoaderButton';
|
import LoaderButton from '@/components/primitives/LoaderButton';
|
||||||
import IconRecents from '@/components/icons/IconRecents';
|
import IconRecents from '@/components/icons/IconRecents';
|
||||||
@ -311,12 +310,12 @@ export default function CommandKClient({
|
|||||||
, [_years, queryLive]);
|
, [_years, queryLive]);
|
||||||
|
|
||||||
const tags = useMemo(() => {
|
const tags = useMemo(() => {
|
||||||
const tagsIncludingHidden = photosCountHidden > 0
|
const tagsIncludingPrivate = photosCountHidden > 0
|
||||||
? addHiddenToTags(_tags, photosCountHidden)
|
? addPrivateToTags(_tags, photosCountHidden)
|
||||||
: _tags;
|
: _tags;
|
||||||
return HIDE_TAGS_WITH_ONE_PHOTO
|
return HIDE_TAGS_WITH_ONE_PHOTO
|
||||||
? limitTagsByCount(tagsIncludingHidden, 2, queryLive)
|
? limitTagsByCount(tagsIncludingPrivate, 2, queryLive)
|
||||||
: tagsIncludingHidden;
|
: tagsIncludingPrivate;
|
||||||
}, [_tags, photosCountHidden, queryLive]);
|
}, [_tags, photosCountHidden, queryLive]);
|
||||||
|
|
||||||
const categorySections: CommandKSection[] = useMemo(() =>
|
const categorySections: CommandKSection[] = useMemo(() =>
|
||||||
@ -380,10 +379,10 @@ export default function CommandKClient({
|
|||||||
className="translate-y-[-0.5px]"
|
className="translate-y-[-0.5px]"
|
||||||
highlight
|
highlight
|
||||||
/>}
|
/>}
|
||||||
{isTagHidden(tag) &&
|
{isTagPrivate(tag) &&
|
||||||
<IconHidden
|
<IconLock
|
||||||
size={15}
|
size={12}
|
||||||
className="translate-y-[-0.5px]"
|
className="text-dim translate-y-[-0.5px]"
|
||||||
/>}
|
/>}
|
||||||
</span>,
|
</span>,
|
||||||
annotation: formatCount(count),
|
annotation: formatCount(count),
|
||||||
@ -504,11 +503,11 @@ export default function CommandKClient({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const pageFeed: CommandKItem = {
|
const pageFull: CommandKItem = {
|
||||||
label: GRID_HOMEPAGE_ENABLED
|
label: GRID_HOMEPAGE_ENABLED
|
||||||
? appText.nav.feed
|
? appText.nav.full
|
||||||
: `${appText.nav.feed} (${appText.nav.home})`,
|
: `${appText.nav.full} (${appText.nav.home})`,
|
||||||
path: PATH_FEED_INFERRED,
|
path: PATH_FULL_INFERRED,
|
||||||
};
|
};
|
||||||
|
|
||||||
const pageGrid: CommandKItem = {
|
const pageGrid: CommandKItem = {
|
||||||
@ -519,8 +518,8 @@ export default function CommandKClient({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const pageItems: CommandKItem[] = GRID_HOMEPAGE_ENABLED
|
const pageItems: CommandKItem[] = GRID_HOMEPAGE_ENABLED
|
||||||
? [pageGrid, pageFeed]
|
? [pageGrid, pageFull]
|
||||||
: [pageFeed, pageGrid];
|
: [pageFull, pageGrid];
|
||||||
|
|
||||||
const sectionPages: CommandKSection = {
|
const sectionPages: CommandKSection = {
|
||||||
heading: 'Pages',
|
heading: 'Pages',
|
||||||
|
|||||||
@ -2,10 +2,13 @@ import clsx from 'clsx/lite';
|
|||||||
import { InputHTMLAttributes, ReactNode, RefObject } from 'react';
|
import { InputHTMLAttributes, ReactNode, RefObject } from 'react';
|
||||||
import { ImCheckmark } from 'react-icons/im';
|
import { ImCheckmark } from 'react-icons/im';
|
||||||
|
|
||||||
|
const SIZE = 'size-4.5';
|
||||||
|
|
||||||
const boxStyles = clsx(
|
const boxStyles = clsx(
|
||||||
'relative',
|
'relative',
|
||||||
'inline-flex items-center justify-center',
|
'inline-flex items-center justify-center',
|
||||||
'size-5 rounded-md border',
|
'rounded-md border',
|
||||||
|
SIZE,
|
||||||
);
|
);
|
||||||
|
|
||||||
export default function Checkbox({
|
export default function Checkbox({
|
||||||
@ -22,7 +25,7 @@ export default function Checkbox({
|
|||||||
<span
|
<span
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'relative inline-flex items-center justify-center',
|
'relative inline-flex items-center justify-center',
|
||||||
'size-5',
|
SIZE,
|
||||||
props.readOnly
|
props.readOnly
|
||||||
? 'cursor-not-allowed'
|
? 'cursor-not-allowed'
|
||||||
: 'group-has-active:opacity-70',
|
: 'group-has-active:opacity-70',
|
||||||
@ -40,7 +43,7 @@ export default function Checkbox({
|
|||||||
: 'bg-black',
|
: 'bg-black',
|
||||||
)}>
|
)}>
|
||||||
<ImCheckmark
|
<ImCheckmark
|
||||||
size={12}
|
size={11}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'text-white',
|
'text-white',
|
||||||
props.readOnly && 'dark:text-gray-400',
|
props.readOnly && 'dark:text-gray-400',
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import { FiChevronDown } from 'react-icons/fi';
|
|||||||
import { parameterize } from '@/utility/string';
|
import { parameterize } from '@/utility/string';
|
||||||
import Checkbox from './Checkbox';
|
import Checkbox from './Checkbox';
|
||||||
import ResponsiveText from './primitives/ResponsiveText';
|
import ResponsiveText from './primitives/ResponsiveText';
|
||||||
|
import Tooltip from './Tooltip';
|
||||||
|
|
||||||
export default function FieldSetWithStatus({
|
export default function FieldSetWithStatus({
|
||||||
id: _id,
|
id: _id,
|
||||||
@ -17,6 +18,7 @@ export default function FieldSetWithStatus({
|
|||||||
icon,
|
icon,
|
||||||
note,
|
note,
|
||||||
noteShort,
|
noteShort,
|
||||||
|
tooltip,
|
||||||
error,
|
error,
|
||||||
value,
|
value,
|
||||||
isModified,
|
isModified,
|
||||||
@ -45,6 +47,7 @@ export default function FieldSetWithStatus({
|
|||||||
icon?: ReactNode
|
icon?: ReactNode
|
||||||
note?: string
|
note?: string
|
||||||
noteShort?: string
|
noteShort?: string
|
||||||
|
tooltip?: string
|
||||||
error?: string
|
error?: string
|
||||||
value: string
|
value: string
|
||||||
isModified?: boolean
|
isModified?: boolean
|
||||||
@ -116,7 +119,7 @@ export default function FieldSetWithStatus({
|
|||||||
// For managing checkbox active state
|
// For managing checkbox active state
|
||||||
'group',
|
'group',
|
||||||
'space-y-1',
|
'space-y-1',
|
||||||
type === 'checkbox' && 'flex items-center gap-3',
|
type === 'checkbox' && 'flex items-center gap-2',
|
||||||
className,
|
className,
|
||||||
)}>
|
)}>
|
||||||
{!hideLabel &&
|
{!hideLabel &&
|
||||||
@ -124,17 +127,28 @@ export default function FieldSetWithStatus({
|
|||||||
htmlFor={id}
|
htmlFor={id}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'inline-flex flex-wrap gap-x-2 items-center select-none',
|
'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',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className="inline-flex items-center gap-x-1.5">
|
<span className="inline-flex items-center gap-x-[5px]">
|
||||||
{icon && <span
|
{icon &&
|
||||||
className="inline-flex items-center justify-center w-4"
|
<span className={clsx(
|
||||||
>
|
'inline-flex items-center justify-center w-4 shrink-0',
|
||||||
|
)}>
|
||||||
{icon}
|
{icon}
|
||||||
</span>}
|
</span>}
|
||||||
|
<span className="truncate">
|
||||||
{label}
|
{label}
|
||||||
</span>
|
</span>
|
||||||
|
{tooltip &&
|
||||||
|
<Tooltip
|
||||||
|
content={tooltip}
|
||||||
|
classNameTrigger="translate-y-[-1.5px] text-dim"
|
||||||
|
supportMobile
|
||||||
|
/>}
|
||||||
|
</span>
|
||||||
{note && !error &&
|
{note && !error &&
|
||||||
<ResponsiveText
|
<ResponsiveText
|
||||||
className="text-gray-400 dark:text-gray-600"
|
className="text-gray-400 dark:text-gray-600"
|
||||||
|
|||||||
@ -110,8 +110,10 @@ export default function EntityLink({
|
|||||||
setIsLoading={setIsLoading}
|
setIsLoading={setIsLoading}
|
||||||
>
|
>
|
||||||
<LabeledIcon {...{
|
<LabeledIcon {...{
|
||||||
icon: (hasBadgeIcon && !useForHover) ? undefined : icon,
|
icon:
|
||||||
iconWide: (hasBadgeIcon && !useForHover) ? undefined : iconWide,
|
(badged && hasBadgeIcon && !useForHover) ? undefined : icon,
|
||||||
|
iconWide:
|
||||||
|
(badged && hasBadgeIcon && !useForHover) ? undefined : iconWide,
|
||||||
prefetch,
|
prefetch,
|
||||||
title,
|
title,
|
||||||
type: useForHover ? 'icon-first' : type,
|
type: useForHover ? 'icon-first' : type,
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
const INTRINSIC_WIDTH = 28;
|
const INTRINSIC_WIDTH = 28;
|
||||||
const INTRINSIC_HEIGHT = 24;
|
const INTRINSIC_HEIGHT = 24;
|
||||||
|
|
||||||
export default function IconFeed({
|
export default function IconFull({
|
||||||
width = INTRINSIC_WIDTH,
|
width = INTRINSIC_WIDTH,
|
||||||
includeTitle = true,
|
includeTitle = true,
|
||||||
className,
|
className,
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
import clsx from 'clsx/lite';
|
||||||
import { IconBaseProps } from 'react-icons';
|
import { IconBaseProps } from 'react-icons';
|
||||||
import { AiOutlineEyeInvisible, AiOutlineEye } from 'react-icons/ai';
|
import { AiOutlineEyeInvisible, AiOutlineEye } from 'react-icons/ai';
|
||||||
|
|
||||||
@ -7,6 +8,8 @@ export default function IconHidden({
|
|||||||
}: IconBaseProps & {
|
}: IconBaseProps & {
|
||||||
visible?: boolean
|
visible?: boolean
|
||||||
}) {
|
}) {
|
||||||
|
// Flip so slash goes left to right
|
||||||
|
props.className = clsx('-scale-x-100', props.className);
|
||||||
return visible
|
return visible
|
||||||
? <AiOutlineEye {...props} />
|
? <AiOutlineEye {...props} />
|
||||||
: <AiOutlineEyeInvisible {...props} />;
|
: <AiOutlineEyeInvisible {...props} />;
|
||||||
|
|||||||
@ -1,12 +1,19 @@
|
|||||||
import { IconBaseProps } from 'react-icons';
|
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';
|
import { FiLock } from 'react-icons/fi';
|
||||||
|
|
||||||
export default function IconLock({
|
export default function IconLock({
|
||||||
|
solid,
|
||||||
narrow,
|
narrow,
|
||||||
|
open,
|
||||||
...props
|
...props
|
||||||
}: IconBaseProps & { narrow?: boolean }) {
|
}: IconBaseProps & { solid?: boolean, narrow?: boolean, open?: boolean }) {
|
||||||
return narrow
|
if (solid) {
|
||||||
? <BiLockAlt {...props} />
|
return open ? <FaLockOpen {...props} /> : <FaLock {...props} />;
|
||||||
: <FiLock {...props} />;
|
} else if (narrow) {
|
||||||
|
return open ? <BiLockOpenAlt {...props} /> : <BiLockAlt {...props} />;
|
||||||
|
} else {
|
||||||
|
return open ? <FiLock {...props} /> : <FiLock {...props} />;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,32 +1,43 @@
|
|||||||
import { descriptionForPhoto, Photo, titleForPhoto } from '@/photo';
|
import { USER_DEFAULT_SORT_OPTIONS } from '@/app/config';
|
||||||
|
import { PhotoQueryOptions } from '../photo/db';
|
||||||
import {
|
import {
|
||||||
getNextImageUrlForRequest,
|
INFINITE_SCROLL_FULL_INITIAL,
|
||||||
NextImageSize,
|
INFINITE_SCROLL_GRID_INITIAL,
|
||||||
} from '@/platforms/next-image';
|
} 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;
|
// PAGE FEED QUERY OPTIONS
|
||||||
export const FEED_PHOTO_WIDTH_MEDIUM = 640;
|
|
||||||
export const FEED_PHOTO_WIDTH_LARGE = 1200;
|
|
||||||
|
|
||||||
export interface FeedMedia {
|
export const getFeedQueryOptions = ({
|
||||||
url: string
|
isGrid,
|
||||||
width: number
|
sortBy = USER_DEFAULT_SORT_OPTIONS.sortBy,
|
||||||
height: number
|
sortWithPriority = USER_DEFAULT_SORT_OPTIONS.sortWithPriority,
|
||||||
}
|
}: {
|
||||||
|
isGrid: boolean,
|
||||||
export const generateFeedMedia = (
|
sortBy?: SortBy,
|
||||||
photo: Photo,
|
sortWithPriority?: boolean,
|
||||||
size: NextImageSize,
|
}): PhotoQueryOptions => ({
|
||||||
): FeedMedia => ({
|
...FEED_BASE_QUERY_OPTIONS,
|
||||||
url: getNextImageUrlForRequest({ imageUrl: photo.url, size }),
|
sortBy,
|
||||||
width: size,
|
sortWithPriority,
|
||||||
height: Math.round(size / photo.aspectRatio),
|
limit: isGrid
|
||||||
|
? INFINITE_SCROLL_GRID_INITIAL
|
||||||
|
: INFINITE_SCROLL_FULL_INITIAL,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getCoreFeedFields = (photo: Photo) => ({
|
export const FEED_META_QUERY_OPTIONS: PhotoQueryOptions = {
|
||||||
id: photo.id,
|
...FEED_BASE_QUERY_OPTIONS,
|
||||||
title: titleForPhoto(photo),
|
};
|
||||||
description: descriptionForPhoto(photo, true),
|
|
||||||
});
|
// PROGRAMMATIC FEED QUERY OPTIONS
|
||||||
|
|
||||||
|
export const PROGRAMMATIC_QUERY_OPTIONS: PhotoQueryOptions = {
|
||||||
|
...FEED_BASE_QUERY_OPTIONS,
|
||||||
|
sortBy: 'createdAt',
|
||||||
|
limit: FEED_PHOTO_REQUEST_LIMIT,
|
||||||
|
};
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import {
|
|||||||
FeedMedia,
|
FeedMedia,
|
||||||
generateFeedMedia,
|
generateFeedMedia,
|
||||||
getCoreFeedFields,
|
getCoreFeedFields,
|
||||||
} from '.';
|
} from './programmatic';
|
||||||
import { formatDateFromPostgresString } from '@/utility/date';
|
import { formatDateFromPostgresString } from '@/utility/date';
|
||||||
import { Photo } from '@/photo';
|
import { Photo } from '@/photo';
|
||||||
import { BASE_URL, META_DESCRIPTION, META_TITLE } from '@/app/config';
|
import { BASE_URL, META_DESCRIPTION, META_TITLE } from '@/app/config';
|
||||||
|
|||||||
32
src/feed/programmatic.ts
Normal file
32
src/feed/programmatic.ts
Normal file
@ -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),
|
||||||
|
});
|
||||||
@ -5,8 +5,8 @@ import {
|
|||||||
FeedMedia,
|
FeedMedia,
|
||||||
generateFeedMedia,
|
generateFeedMedia,
|
||||||
getCoreFeedFields,
|
getCoreFeedFields,
|
||||||
} from '.';
|
} from './programmatic';
|
||||||
import { ABSOLUTE_PATH_FOR_RSS_XML, absolutePathForPhoto } from '@/app/paths';
|
import { ABSOLUTE_PATH_RSS_XML, absolutePathForPhoto } from '@/app/paths';
|
||||||
import { formatDate } from '@/utility/date';
|
import { formatDate } from '@/utility/date';
|
||||||
import { formatStringForXml } from '@/utility/string';
|
import { formatStringForXml } from '@/utility/string';
|
||||||
import { BASE_URL, META_DESCRIPTION, META_TITLE } from '@/app/config';
|
import { BASE_URL, META_DESCRIPTION, META_TITLE } from '@/app/config';
|
||||||
@ -67,7 +67,7 @@ export const formatFeedRssXml = (photos: Photo[]) =>
|
|||||||
<channel>
|
<channel>
|
||||||
<title>${META_TITLE}</title>
|
<title>${META_TITLE}</title>
|
||||||
<atom:link
|
<atom:link
|
||||||
href="${ABSOLUTE_PATH_FOR_RSS_XML}"
|
href="${ABSOLUTE_PATH_RSS_XML}"
|
||||||
rel="self"
|
rel="self"
|
||||||
type="application/rss+xml"
|
type="application/rss+xml"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -44,7 +44,7 @@ export const TEXT: I18N = {
|
|||||||
},
|
},
|
||||||
nav: {
|
nav: {
|
||||||
home: 'হোম',
|
home: 'হোম',
|
||||||
feed: 'ফিড',
|
full: 'সম্পূর্ণ',
|
||||||
grid: 'গ্রিড',
|
grid: 'গ্রিড',
|
||||||
admin: 'অ্যাডমিন',
|
admin: 'অ্যাডমিন',
|
||||||
search: 'সার্চ',
|
search: 'সার্চ',
|
||||||
@ -103,8 +103,8 @@ export const TEXT: I18N = {
|
|||||||
edit: 'এডিট',
|
edit: 'এডিট',
|
||||||
favorite: 'পছন্দ',
|
favorite: 'পছন্দ',
|
||||||
unfavorite: 'পছন্দ অপসারণ',
|
unfavorite: 'পছন্দ অপসারণ',
|
||||||
hide: 'লুকান',
|
private: 'ব্যক্তিগত করুন',
|
||||||
unhide: 'দেখান',
|
public: 'সর্বজনীন করুন',
|
||||||
download: 'ডাউনলোড',
|
download: 'ডাউনলোড',
|
||||||
sync: 'সিঙ্ক',
|
sync: 'সিঙ্ক',
|
||||||
delete: 'ডিলিট',
|
delete: 'ডিলিট',
|
||||||
|
|||||||
@ -42,7 +42,7 @@ export const TEXT = {
|
|||||||
},
|
},
|
||||||
nav: {
|
nav: {
|
||||||
home: 'Home',
|
home: 'Home',
|
||||||
feed: 'Feed',
|
full: 'Full',
|
||||||
grid: 'Grid',
|
grid: 'Grid',
|
||||||
admin: 'Admin',
|
admin: 'Admin',
|
||||||
search: 'Search',
|
search: 'Search',
|
||||||
@ -101,8 +101,8 @@ export const TEXT = {
|
|||||||
edit: 'Edit',
|
edit: 'Edit',
|
||||||
favorite: 'Favorite',
|
favorite: 'Favorite',
|
||||||
unfavorite: 'Unfavorite',
|
unfavorite: 'Unfavorite',
|
||||||
hide: 'Hide',
|
private: 'Make Private',
|
||||||
unhide: 'Unhide',
|
public: 'Make Public',
|
||||||
download: 'Download',
|
download: 'Download',
|
||||||
sync: 'Sync',
|
sync: 'Sync',
|
||||||
delete: 'Delete',
|
delete: 'Delete',
|
||||||
|
|||||||
@ -43,7 +43,7 @@ export const TEXT: I18N = {
|
|||||||
},
|
},
|
||||||
nav: {
|
nav: {
|
||||||
home: 'Beranda',
|
home: 'Beranda',
|
||||||
feed: 'Umpan',
|
full: 'Lengkap',
|
||||||
grid: 'Grid',
|
grid: 'Grid',
|
||||||
admin: 'Admin',
|
admin: 'Admin',
|
||||||
search: 'Cari',
|
search: 'Cari',
|
||||||
@ -102,8 +102,8 @@ export const TEXT: I18N = {
|
|||||||
edit: 'Edit',
|
edit: 'Edit',
|
||||||
favorite: 'Favorit',
|
favorite: 'Favorit',
|
||||||
unfavorite: 'Hapus dari Favorit',
|
unfavorite: 'Hapus dari Favorit',
|
||||||
hide: 'Sembunyikan',
|
private: 'Buat Privat',
|
||||||
unhide: 'Tampilkan',
|
public: 'Buat Publik',
|
||||||
download: 'Unduh',
|
download: 'Unduh',
|
||||||
sync: 'Sinkronkan',
|
sync: 'Sinkronkan',
|
||||||
delete: 'Hapus',
|
delete: 'Hapus',
|
||||||
|
|||||||
@ -43,7 +43,7 @@ export const TEXT: I18N = {
|
|||||||
},
|
},
|
||||||
nav: {
|
nav: {
|
||||||
home: 'Início',
|
home: 'Início',
|
||||||
feed: 'Feed',
|
full: 'Completo',
|
||||||
grid: 'Grade',
|
grid: 'Grade',
|
||||||
admin: 'Menu de administrador',
|
admin: 'Menu de administrador',
|
||||||
search: 'Pesquisar',
|
search: 'Pesquisar',
|
||||||
@ -102,8 +102,8 @@ export const TEXT: I18N = {
|
|||||||
edit: 'Editar',
|
edit: 'Editar',
|
||||||
favorite: 'Favoritar',
|
favorite: 'Favoritar',
|
||||||
unfavorite: 'Remover dos favoritos',
|
unfavorite: 'Remover dos favoritos',
|
||||||
hide: 'Ocultar',
|
private: 'Tornar Privado',
|
||||||
unhide: 'Mostrar',
|
public: 'Tornar Público',
|
||||||
download: 'Baixar',
|
download: 'Baixar',
|
||||||
sync: 'Sincronizar',
|
sync: 'Sincronizar',
|
||||||
delete: 'Excluir',
|
delete: 'Excluir',
|
||||||
|
|||||||
@ -43,7 +43,7 @@ export const TEXT: I18N = {
|
|||||||
},
|
},
|
||||||
nav: {
|
nav: {
|
||||||
home: 'Início',
|
home: 'Início',
|
||||||
feed: 'Feed',
|
full: 'Completo',
|
||||||
grid: 'Grade',
|
grid: 'Grade',
|
||||||
admin: 'Menu de administração',
|
admin: 'Menu de administração',
|
||||||
search: 'Pesquisar',
|
search: 'Pesquisar',
|
||||||
@ -102,8 +102,8 @@ export const TEXT: I18N = {
|
|||||||
edit: 'Editar',
|
edit: 'Editar',
|
||||||
favorite: 'Favoritar',
|
favorite: 'Favoritar',
|
||||||
unfavorite: 'Remover dos favoritos',
|
unfavorite: 'Remover dos favoritos',
|
||||||
hide: 'Ocultar',
|
private: 'Tornar Privado',
|
||||||
unhide: 'Mostrar',
|
public: 'Tornar Público',
|
||||||
download: 'Descarregar',
|
download: 'Descarregar',
|
||||||
sync: 'Sincronizar',
|
sync: 'Sincronizar',
|
||||||
delete: 'Excluir',
|
delete: 'Excluir',
|
||||||
|
|||||||
@ -43,7 +43,7 @@ export const TEXT: I18N = {
|
|||||||
},
|
},
|
||||||
nav: {
|
nav: {
|
||||||
home: '首页',
|
home: '首页',
|
||||||
feed: '动态',
|
full: '完整',
|
||||||
grid: '网格',
|
grid: '网格',
|
||||||
admin: '管理',
|
admin: '管理',
|
||||||
search: '搜索',
|
search: '搜索',
|
||||||
@ -102,8 +102,8 @@ export const TEXT: I18N = {
|
|||||||
edit: '编辑',
|
edit: '编辑',
|
||||||
favorite: '收藏',
|
favorite: '收藏',
|
||||||
unfavorite: '取消收藏',
|
unfavorite: '取消收藏',
|
||||||
hide: '隐藏',
|
private: '设为私密',
|
||||||
unhide: '取消隐藏',
|
public: '设为公开',
|
||||||
download: '下载',
|
download: '下载',
|
||||||
sync: '同步',
|
sync: '同步',
|
||||||
delete: '删除',
|
delete: '删除',
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { Photo } from '../photo';
|
import { Photo } from '../photo';
|
||||||
import IconFeed from '@/components/icons/IconFeed';
|
import IconFull from '@/components/icons/IconFull';
|
||||||
import IconGrid from '@/components/icons/IconGrid';
|
import IconGrid from '@/components/icons/IconGrid';
|
||||||
import ImagePhotoGrid from './components/ImagePhotoGrid';
|
import ImagePhotoGrid from './components/ImagePhotoGrid';
|
||||||
import { NextImageSize } from '@/platforms/next-image';
|
import { NextImageSize } from '@/platforms/next-image';
|
||||||
@ -66,7 +66,7 @@ export default function TemplateImageResponse({
|
|||||||
color: '#333',
|
color: '#333',
|
||||||
borderRight: '2px solid #333',
|
borderRight: '2px solid #333',
|
||||||
}}>
|
}}>
|
||||||
<IconFeed includeTitle={false} width={80} />
|
<IconFull includeTitle={false} width={80} />
|
||||||
</div>
|
</div>
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
|||||||
@ -35,6 +35,7 @@ export default function InfinitePhotoScroll({
|
|||||||
itemsPerPage,
|
itemsPerPage,
|
||||||
sortBy,
|
sortBy,
|
||||||
sortWithPriority,
|
sortWithPriority,
|
||||||
|
excludeFromFeeds,
|
||||||
camera,
|
camera,
|
||||||
lens,
|
lens,
|
||||||
tag,
|
tag,
|
||||||
@ -50,6 +51,7 @@ export default function InfinitePhotoScroll({
|
|||||||
itemsPerPage: number
|
itemsPerPage: number
|
||||||
sortBy?: SortBy
|
sortBy?: SortBy
|
||||||
sortWithPriority?: boolean
|
sortWithPriority?: boolean
|
||||||
|
excludeFromFeeds?: boolean
|
||||||
cacheKey: string
|
cacheKey: string
|
||||||
wrapMoreButtonInGrid?: boolean
|
wrapMoreButtonInGrid?: boolean
|
||||||
useCachedPhotos?: boolean
|
useCachedPhotos?: boolean
|
||||||
@ -77,6 +79,7 @@ export default function InfinitePhotoScroll({
|
|||||||
offset: initialOffset + getSizeFromKey(keyWithSize) * itemsPerPage,
|
offset: initialOffset + getSizeFromKey(keyWithSize) * itemsPerPage,
|
||||||
sortBy,
|
sortBy,
|
||||||
sortWithPriority,
|
sortWithPriority,
|
||||||
|
excludeFromFeeds,
|
||||||
limit: itemsPerPage,
|
limit: itemsPerPage,
|
||||||
hidden: includeHiddenPhotos ? 'include' : 'exclude',
|
hidden: includeHiddenPhotos ? 'include' : 'exclude',
|
||||||
camera,
|
camera,
|
||||||
@ -90,6 +93,7 @@ export default function InfinitePhotoScroll({
|
|||||||
useCachedPhotos,
|
useCachedPhotos,
|
||||||
sortBy,
|
sortBy,
|
||||||
sortWithPriority,
|
sortWithPriority,
|
||||||
|
excludeFromFeeds,
|
||||||
initialOffset,
|
initialOffset,
|
||||||
itemsPerPage,
|
itemsPerPage,
|
||||||
includeHiddenPhotos,
|
includeHiddenPhotos,
|
||||||
|
|||||||
@ -7,8 +7,8 @@ import PhotoGrid from './PhotoGrid';
|
|||||||
import TagHeader from '@/tag/TagHeader';
|
import TagHeader from '@/tag/TagHeader';
|
||||||
import CameraHeader from '@/camera/CameraHeader';
|
import CameraHeader from '@/camera/CameraHeader';
|
||||||
import FilmHeader from '@/film/FilmHeader';
|
import FilmHeader from '@/film/FilmHeader';
|
||||||
import { TAG_HIDDEN } from '@/tag';
|
import { TAG_PRIVATE } from '@/tag';
|
||||||
import HiddenHeader from '@/tag/HiddenHeader';
|
import PrivateHeader from '@/tag/PrivateHeader';
|
||||||
import FocalLengthHeader from '@/focal/FocalLengthHeader';
|
import FocalLengthHeader from '@/focal/FocalLengthHeader';
|
||||||
import PhotoHeader from './PhotoHeader';
|
import PhotoHeader from './PhotoHeader';
|
||||||
import RecipeHeader from '@/recipe/RecipeHeader';
|
import RecipeHeader from '@/recipe/RecipeHeader';
|
||||||
@ -48,8 +48,8 @@ export default function PhotoDetailPage({
|
|||||||
let customHeader: ReactNode | undefined;
|
let customHeader: ReactNode | undefined;
|
||||||
|
|
||||||
if (tag) {
|
if (tag) {
|
||||||
customHeader = tag === TAG_HIDDEN
|
customHeader = tag === TAG_PRIVATE
|
||||||
? <HiddenHeader
|
? <PrivateHeader
|
||||||
photos={photos}
|
photos={photos}
|
||||||
selectedPhoto={photo}
|
selectedPhoto={photo}
|
||||||
indexNumber={indexNumber}
|
indexNumber={indexNumber}
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import {
|
import {
|
||||||
INFINITE_SCROLL_FEED_MULTIPLE,
|
INFINITE_SCROLL_FULL_MULTIPLE,
|
||||||
Photo,
|
Photo,
|
||||||
} from '.';
|
} from '.';
|
||||||
import PhotosLarge from './PhotosLarge';
|
import PhotosLarge from './PhotosLarge';
|
||||||
import PhotosLargeInfinite from './PhotosLargeInfinite';
|
import PhotosLargeInfinite from './PhotosLargeInfinite';
|
||||||
import { SortBy } from './db/sort';
|
import { SortBy } from './db/sort';
|
||||||
|
|
||||||
export default function PhotoFeedPage({
|
export default function PhotoFullPage({
|
||||||
photos,
|
photos,
|
||||||
photosCount,
|
photosCount,
|
||||||
sortBy,
|
sortBy,
|
||||||
@ -25,7 +25,7 @@ export default function PhotoFeedPage({
|
|||||||
sortBy={sortBy}
|
sortBy={sortBy}
|
||||||
sortWithPriority={sortWithPriority}
|
sortWithPriority={sortWithPriority}
|
||||||
initialOffset={photos.length}
|
initialOffset={photos.length}
|
||||||
itemsPerPage={INFINITE_SCROLL_FEED_MULTIPLE}
|
itemsPerPage={INFINITE_SCROLL_FULL_MULTIPLE}
|
||||||
/>}
|
/>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -15,6 +15,7 @@ export default function PhotoGridContainer({
|
|||||||
count,
|
count,
|
||||||
sortBy,
|
sortBy,
|
||||||
sortWithPriority,
|
sortWithPriority,
|
||||||
|
excludeFromFeeds,
|
||||||
animateOnFirstLoadOnly,
|
animateOnFirstLoadOnly,
|
||||||
header,
|
header,
|
||||||
sidebar,
|
sidebar,
|
||||||
@ -25,6 +26,7 @@ export default function PhotoGridContainer({
|
|||||||
count: number
|
count: number
|
||||||
sortBy?: SortBy
|
sortBy?: SortBy
|
||||||
sortWithPriority?: boolean
|
sortWithPriority?: boolean
|
||||||
|
excludeFromFeeds?: boolean
|
||||||
header?: ReactNode
|
header?: ReactNode
|
||||||
sidebar?: ReactNode
|
sidebar?: ReactNode
|
||||||
} & ComponentProps<typeof PhotoGrid>) {
|
} & ComponentProps<typeof PhotoGrid>) {
|
||||||
@ -61,6 +63,7 @@ export default function PhotoGridContainer({
|
|||||||
initialOffset: photos.length,
|
initialOffset: photos.length,
|
||||||
sortBy,
|
sortBy,
|
||||||
sortWithPriority,
|
sortWithPriority,
|
||||||
|
excludeFromFeeds,
|
||||||
...categories,
|
...categories,
|
||||||
canStart: shouldAnimateDynamicItems,
|
canStart: shouldAnimateDynamicItems,
|
||||||
animateOnFirstLoadOnly,
|
animateOnFirstLoadOnly,
|
||||||
|
|||||||
@ -11,6 +11,7 @@ export default function PhotoGridInfinite({
|
|||||||
initialOffset,
|
initialOffset,
|
||||||
sortBy,
|
sortBy,
|
||||||
sortWithPriority,
|
sortWithPriority,
|
||||||
|
excludeFromFeeds,
|
||||||
canStart,
|
canStart,
|
||||||
animateOnFirstLoadOnly,
|
animateOnFirstLoadOnly,
|
||||||
canSelect,
|
canSelect,
|
||||||
@ -20,6 +21,7 @@ export default function PhotoGridInfinite({
|
|||||||
initialOffset: number
|
initialOffset: number
|
||||||
sortBy?: SortBy
|
sortBy?: SortBy
|
||||||
sortWithPriority?: boolean
|
sortWithPriority?: boolean
|
||||||
|
excludeFromFeeds?: boolean
|
||||||
} & Omit<ComponentProps<typeof PhotoGrid>, 'photos'>) {
|
} & Omit<ComponentProps<typeof PhotoGrid>, 'photos'>) {
|
||||||
return (
|
return (
|
||||||
<InfinitePhotoScroll
|
<InfinitePhotoScroll
|
||||||
@ -28,6 +30,7 @@ export default function PhotoGridInfinite({
|
|||||||
itemsPerPage={INFINITE_SCROLL_GRID_MULTIPLE}
|
itemsPerPage={INFINITE_SCROLL_GRID_MULTIPLE}
|
||||||
sortBy={sortBy}
|
sortBy={sortBy}
|
||||||
sortWithPriority={sortWithPriority}
|
sortWithPriority={sortWithPriority}
|
||||||
|
excludeFromFeeds={excludeFromFeeds}
|
||||||
{...categories}
|
{...categories}
|
||||||
>
|
>
|
||||||
{({ photos, onLastPhotoVisible }) =>
|
{({ photos, onLastPhotoVisible }) =>
|
||||||
|
|||||||
@ -42,6 +42,7 @@ export default function PhotoGridPageClient({
|
|||||||
count={photosCount}
|
count={photosCount}
|
||||||
sortBy={sortBy}
|
sortBy={sortBy}
|
||||||
sortWithPriority={sortWithPriority}
|
sortWithPriority={sortWithPriority}
|
||||||
|
excludeFromFeeds
|
||||||
prioritizeInitialPhotos
|
prioritizeInitialPhotos
|
||||||
sidebar={
|
sidebar={
|
||||||
<MaskedScroll
|
<MaskedScroll
|
||||||
|
|||||||
@ -4,12 +4,17 @@ import PhotoCamera from '@/camera/PhotoCamera';
|
|||||||
import HeaderList from '@/components/HeaderList';
|
import HeaderList from '@/components/HeaderList';
|
||||||
import PhotoTag from '@/tag/PhotoTag';
|
import PhotoTag from '@/tag/PhotoTag';
|
||||||
import { photoQuantityText } from '.';
|
import { photoQuantityText } from '.';
|
||||||
import { TAG_FAVS, TAG_HIDDEN, addHiddenToTags, limitTagsByCount } from '@/tag';
|
import {
|
||||||
|
TAG_FAVS,
|
||||||
|
TAG_PRIVATE,
|
||||||
|
addPrivateToTags,
|
||||||
|
limitTagsByCount,
|
||||||
|
} from '@/tag';
|
||||||
import PhotoFilm from '@/film/PhotoFilm';
|
import PhotoFilm from '@/film/PhotoFilm';
|
||||||
import FavsTag from '../tag/FavsTag';
|
import PhotoFavs from '../tag/PhotoFavs';
|
||||||
import { useAppState } from '@/app/AppState';
|
import { useAppState } from '@/app/AppState';
|
||||||
import { useMemo, useRef } from 'react';
|
import { useMemo, useRef } from 'react';
|
||||||
import HiddenTag from '@/tag/HiddenTag';
|
import PhotoPrivate from '@/tag/PhotoPrivate';
|
||||||
import {
|
import {
|
||||||
CATEGORY_VISIBILITY,
|
CATEGORY_VISIBILITY,
|
||||||
HIDE_TAGS_WITH_ONE_PHOTO,
|
HIDE_TAGS_WITH_ONE_PHOTO,
|
||||||
@ -96,7 +101,7 @@ export default function PhotoGridSidebar({
|
|||||||
const { photosCountHidden } = useAppState();
|
const { photosCountHidden } = useAppState();
|
||||||
|
|
||||||
const tagsIncludingHidden = useMemo(() =>
|
const tagsIncludingHidden = useMemo(() =>
|
||||||
addHiddenToTags(tags, photosCountHidden)
|
addPrivateToTags(tags, photosCountHidden)
|
||||||
, [tags, photosCountHidden]);
|
, [tags, photosCountHidden]);
|
||||||
|
|
||||||
const recentsContent = recents.length > 0
|
const recentsContent = recents.length > 0
|
||||||
@ -196,7 +201,7 @@ export default function PhotoGridSidebar({
|
|||||||
.map(({ tag, count }) => {
|
.map(({ tag, count }) => {
|
||||||
switch (tag) {
|
switch (tag) {
|
||||||
case TAG_FAVS:
|
case TAG_FAVS:
|
||||||
return <FavsTag
|
return <PhotoFavs
|
||||||
key={TAG_FAVS}
|
key={TAG_FAVS}
|
||||||
countOnHover={count}
|
countOnHover={count}
|
||||||
type="icon-last"
|
type="icon-last"
|
||||||
@ -204,9 +209,9 @@ export default function PhotoGridSidebar({
|
|||||||
contrast="low"
|
contrast="low"
|
||||||
badged
|
badged
|
||||||
/>;
|
/>;
|
||||||
case TAG_HIDDEN:
|
case TAG_PRIVATE:
|
||||||
return <HiddenTag
|
return <PhotoPrivate
|
||||||
key={TAG_HIDDEN}
|
key={TAG_PRIVATE}
|
||||||
countOnHover={count}
|
countOnHover={count}
|
||||||
type="icon-last"
|
type="icon-last"
|
||||||
prefetch={false}
|
prefetch={false}
|
||||||
|
|||||||
@ -20,7 +20,7 @@ import {
|
|||||||
deletePhotoAction,
|
deletePhotoAction,
|
||||||
syncPhotoAction,
|
syncPhotoAction,
|
||||||
toggleFavoritePhotoAction,
|
toggleFavoritePhotoAction,
|
||||||
toggleHidePhotoAction,
|
togglePrivatePhotoAction,
|
||||||
} from './actions';
|
} from './actions';
|
||||||
import { isPhotoFav } from '@/tag';
|
import { isPhotoFav } from '@/tag';
|
||||||
import Tooltip from '@/components/Tooltip';
|
import Tooltip from '@/components/Tooltip';
|
||||||
@ -68,7 +68,7 @@ export default function PhotoPrevNextActions({
|
|||||||
}, [photo?.id]);
|
}, [photo?.id]);
|
||||||
|
|
||||||
const toggleHidden = useCallback(() => {
|
const toggleHidden = useCallback(() => {
|
||||||
if (photo?.id) { return toggleHidePhotoAction(photo.id); }
|
if (photo?.id) { return togglePrivatePhotoAction(photo.id); }
|
||||||
}, [photo?.id]);
|
}, [photo?.id]);
|
||||||
|
|
||||||
const navigateToPhotoEdit = useNavigateOrRunActionWithToast({
|
const navigateToPhotoEdit = useNavigateOrRunActionWithToast({
|
||||||
@ -168,7 +168,7 @@ export default function PhotoPrevNextActions({
|
|||||||
unfavoritePhoto();
|
unfavoritePhoto();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case KEY_COMMANDS.toggleHide:
|
case KEY_COMMANDS.togglePrivate:
|
||||||
if (isUserSignedIn && photo) {
|
if (isUserSignedIn && photo) {
|
||||||
if (photo.hidden) {
|
if (photo.hidden) {
|
||||||
unhidePhoto();
|
unhidePhoto();
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { PATH_FEED_INFERRED } from '@/app/paths';
|
import { PATH_FULL_INFERRED } from '@/app/paths';
|
||||||
import InfinitePhotoScroll from './InfinitePhotoScroll';
|
import InfinitePhotoScroll from './InfinitePhotoScroll';
|
||||||
import PhotosLarge from './PhotosLarge';
|
import PhotosLarge from './PhotosLarge';
|
||||||
import { SortBy } from './db/sort';
|
import { SortBy } from './db/sort';
|
||||||
@ -17,7 +17,7 @@ export default function PhotosLargeInfinite({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<InfinitePhotoScroll
|
<InfinitePhotoScroll
|
||||||
cacheKey={`page-${PATH_FEED_INFERRED}`}
|
cacheKey={`page-${PATH_FULL_INFERRED}`}
|
||||||
initialOffset={initialOffset}
|
initialOffset={initialOffset}
|
||||||
itemsPerPage={itemsPerPage}
|
itemsPerPage={itemsPerPage}
|
||||||
sortBy={sortBy}
|
sortBy={sortBy}
|
||||||
|
|||||||
@ -94,6 +94,7 @@ const addUpload = async ({
|
|||||||
tags,
|
tags,
|
||||||
favorite,
|
favorite,
|
||||||
hidden,
|
hidden,
|
||||||
|
excludeFromFeeds,
|
||||||
takenAtLocal,
|
takenAtLocal,
|
||||||
takenAtNaiveLocal,
|
takenAtNaiveLocal,
|
||||||
onStreamUpdate,
|
onStreamUpdate,
|
||||||
@ -104,6 +105,7 @@ const addUpload = async ({
|
|||||||
tags?: string
|
tags?: string
|
||||||
favorite?: string
|
favorite?: string
|
||||||
hidden?: string
|
hidden?: string
|
||||||
|
excludeFromFeeds?: string
|
||||||
takenAtLocal: string
|
takenAtLocal: string
|
||||||
takenAtNaiveLocal: string
|
takenAtNaiveLocal: string
|
||||||
onStreamUpdate?: (
|
onStreamUpdate?: (
|
||||||
@ -147,6 +149,7 @@ const addUpload = async ({
|
|||||||
title: title || aiTitle,
|
title: title || aiTitle,
|
||||||
caption,
|
caption,
|
||||||
tags: tags || aiTags,
|
tags: tags || aiTags,
|
||||||
|
excludeFromFeeds,
|
||||||
hidden,
|
hidden,
|
||||||
favorite,
|
favorite,
|
||||||
semanticDescription,
|
semanticDescription,
|
||||||
@ -187,6 +190,7 @@ export const addUploadsAction = async ({
|
|||||||
tags,
|
tags,
|
||||||
favorite,
|
favorite,
|
||||||
hidden,
|
hidden,
|
||||||
|
excludeFromFeeds,
|
||||||
takenAtLocal,
|
takenAtLocal,
|
||||||
takenAtNaiveLocal,
|
takenAtNaiveLocal,
|
||||||
}: Omit<
|
}: Omit<
|
||||||
@ -231,6 +235,7 @@ export const addUploadsAction = async ({
|
|||||||
tags,
|
tags,
|
||||||
favorite,
|
favorite,
|
||||||
hidden,
|
hidden,
|
||||||
|
excludeFromFeeds,
|
||||||
takenAtLocal,
|
takenAtLocal,
|
||||||
takenAtNaiveLocal,
|
takenAtNaiveLocal,
|
||||||
onStreamUpdate: streamUpdate,
|
onStreamUpdate: streamUpdate,
|
||||||
@ -317,7 +322,7 @@ export const toggleFavoritePhotoAction = async (
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const toggleHidePhotoAction = async (
|
export const togglePrivatePhotoAction = async (
|
||||||
photoId: string,
|
photoId: string,
|
||||||
redirectPath?: string,
|
redirectPath?: string,
|
||||||
) =>
|
) =>
|
||||||
|
|||||||
@ -25,7 +25,7 @@ import {
|
|||||||
PATHS_ADMIN,
|
PATHS_ADMIN,
|
||||||
PATHS_TO_CACHE,
|
PATHS_TO_CACHE,
|
||||||
PATH_ADMIN,
|
PATH_ADMIN,
|
||||||
PATH_FEED,
|
PATH_FULL,
|
||||||
PATH_GRID,
|
PATH_GRID,
|
||||||
PATH_ROOT,
|
PATH_ROOT,
|
||||||
PREFIX_CAMERA,
|
PREFIX_CAMERA,
|
||||||
@ -153,7 +153,7 @@ export const revalidatePhoto = (photoId: string) => {
|
|||||||
revalidatePath(pathForPhoto({ photo: photoId }), 'layout');
|
revalidatePath(pathForPhoto({ photo: photoId }), 'layout');
|
||||||
revalidatePath(PATH_ROOT, 'layout');
|
revalidatePath(PATH_ROOT, 'layout');
|
||||||
revalidatePath(PATH_GRID, 'layout');
|
revalidatePath(PATH_GRID, 'layout');
|
||||||
revalidatePath(PATH_FEED, 'layout');
|
revalidatePath(PATH_FULL, 'layout');
|
||||||
revalidatePath(PREFIX_TAG, 'layout');
|
revalidatePath(PREFIX_TAG, 'layout');
|
||||||
revalidatePath(PREFIX_CAMERA, 'layout');
|
revalidatePath(PREFIX_CAMERA, 'layout');
|
||||||
revalidatePath(PREFIX_LENS, 'layout');
|
revalidatePath(PREFIX_LENS, 'layout');
|
||||||
@ -179,11 +179,15 @@ export const getPhotosNearIdCached = (
|
|||||||
getPhotosNearId,
|
getPhotosNearId,
|
||||||
[KEY_PHOTOS, ...getPhotosCacheKeys(args[1])],
|
[KEY_PHOTOS, ...getPhotosCacheKeys(args[1])],
|
||||||
)(...args).then(({ photos, indexNumber }) => {
|
)(...args).then(({ photos, indexNumber }) => {
|
||||||
const [photoId, { limit }] = args;
|
const [photoId, { limit }, excludeFromFeeds] = args;
|
||||||
const photo = photos.find(({ id }) => id === photoId);
|
const photo = photos.find(({ id }) => id === photoId);
|
||||||
const isPhotoFirst = photos.findIndex(p => p.id === photoId) === 0;
|
const isPhotoFirst = photos.findIndex(p => p.id === photoId) === 0;
|
||||||
return {
|
return {
|
||||||
photo: photo ? parseCachedPhotoDates(photo) : undefined,
|
photo: photo ? parseCachedPhotoDates(photo) : undefined,
|
||||||
|
// Don't show photo in context when excluded from feeds
|
||||||
|
...excludeFromFeeds && photo?.excludeFromFeeds
|
||||||
|
? { photos: [] }
|
||||||
|
: {
|
||||||
photos: parseCachedPhotosDates(photos),
|
photos: parseCachedPhotosDates(photos),
|
||||||
...limit && {
|
...limit && {
|
||||||
photosGrid: photos.slice(
|
photosGrid: photos.slice(
|
||||||
@ -191,6 +195,7 @@ export const getPhotosNearIdCached = (
|
|||||||
isPhotoFirst ? limit - 1 : limit,
|
isPhotoFirst ? limit - 1 : limit,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
},
|
||||||
indexNumber,
|
indexNumber,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@ -29,6 +29,7 @@ export type PhotoQueryOptions = {
|
|||||||
takenBefore?: Date
|
takenBefore?: Date
|
||||||
takenAfterInclusive?: Date
|
takenAfterInclusive?: Date
|
||||||
updatedBefore?: Date
|
updatedBefore?: Date
|
||||||
|
excludeFromFeeds?: boolean
|
||||||
hidden?: 'exclude' | 'include' | 'only'
|
hidden?: 'exclude' | 'include' | 'only'
|
||||||
} & Omit<PhotoSetCategory, 'camera' | 'lens'> & {
|
} & Omit<PhotoSetCategory, 'camera' | 'lens'> & {
|
||||||
camera?: Partial<Camera>
|
camera?: Partial<Camera>
|
||||||
@ -44,6 +45,7 @@ export const getWheresFromOptions = (
|
|||||||
) => {
|
) => {
|
||||||
const {
|
const {
|
||||||
hidden = 'exclude',
|
hidden = 'exclude',
|
||||||
|
excludeFromFeeds,
|
||||||
takenBefore,
|
takenBefore,
|
||||||
takenAfterInclusive,
|
takenAfterInclusive,
|
||||||
updatedBefore,
|
updatedBefore,
|
||||||
@ -72,6 +74,9 @@ export const getWheresFromOptions = (
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (excludeFromFeeds) {
|
||||||
|
wheres.push('exclude_from_feeds IS NOT TRUE');
|
||||||
|
}
|
||||||
if (takenBefore) {
|
if (takenBefore) {
|
||||||
wheres.push(`taken_at < $${valuesIndex++}`);
|
wheres.push(`taken_at < $${valuesIndex++}`);
|
||||||
wheresValues.push(takenBefore.toISOString());
|
wheresValues.push(takenBefore.toISOString());
|
||||||
|
|||||||
@ -71,6 +71,13 @@ export const MIGRATIONS: Migration[] = [{
|
|||||||
END IF;
|
END IF;
|
||||||
END $$;
|
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) =>
|
export const migrationForError = (e: any) =>
|
||||||
|
|||||||
@ -69,7 +69,8 @@ const createPhotosTable = () =>
|
|||||||
priority_order REAL,
|
priority_order REAL,
|
||||||
taken_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
taken_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
taken_at_naive VARCHAR(255) 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,
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
created_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_title,
|
||||||
recipe_data,
|
recipe_data,
|
||||||
priority_order,
|
priority_order,
|
||||||
|
exclude_from_feeds,
|
||||||
hidden,
|
hidden,
|
||||||
taken_at,
|
taken_at,
|
||||||
taken_at_naive
|
taken_at_naive
|
||||||
@ -224,6 +226,7 @@ export const insertPhoto = (photo: PhotoDbInsert) =>
|
|||||||
${photo.recipeTitle},
|
${photo.recipeTitle},
|
||||||
${photo.recipeData},
|
${photo.recipeData},
|
||||||
${photo.priorityOrder},
|
${photo.priorityOrder},
|
||||||
|
${photo.excludeFromFeeds},
|
||||||
${photo.hidden},
|
${photo.hidden},
|
||||||
${photo.takenAt},
|
${photo.takenAt},
|
||||||
${photo.takenAtNaive}
|
${photo.takenAtNaive}
|
||||||
@ -258,6 +261,7 @@ export const updatePhoto = (photo: PhotoDbInsert) =>
|
|||||||
recipe_title=${photo.recipeTitle},
|
recipe_title=${photo.recipeTitle},
|
||||||
recipe_data=${photo.recipeData},
|
recipe_data=${photo.recipeData},
|
||||||
priority_order=${photo.priorityOrder || null},
|
priority_order=${photo.priorityOrder || null},
|
||||||
|
exclude_from_feeds=${photo.excludeFromFeeds},
|
||||||
hidden=${photo.hidden},
|
hidden=${photo.hidden},
|
||||||
taken_at=${photo.takenAt},
|
taken_at=${photo.takenAt},
|
||||||
taken_at_naive=${photo.takenAtNaive},
|
taken_at_naive=${photo.takenAtNaive},
|
||||||
@ -527,6 +531,7 @@ export const getPhotos = async (options: PhotoQueryOptions = {}) =>
|
|||||||
export const getPhotosNearId = async (
|
export const getPhotosNearId = async (
|
||||||
photoId: string,
|
photoId: string,
|
||||||
options: PhotoQueryOptions,
|
options: PhotoQueryOptions,
|
||||||
|
excludeFromFeeds?: boolean,
|
||||||
) =>
|
) =>
|
||||||
safelyQueryPhotos(async () => {
|
safelyQueryPhotos(async () => {
|
||||||
const { limit } = options;
|
const { limit } = options;
|
||||||
@ -561,6 +566,7 @@ export const getPhotosNearId = async (
|
|||||||
return {
|
return {
|
||||||
photos: rows.map(parsePhotoFromDb),
|
photos: rows.map(parsePhotoFromDb),
|
||||||
indexNumber,
|
indexNumber,
|
||||||
|
excludeFromFeeds,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}, `getPhotosNearId: ${photoId}`);
|
}, `getPhotosNearId: ${photoId}`);
|
||||||
|
|||||||
@ -6,8 +6,8 @@ import {
|
|||||||
PARAM_SORT_ORDER_OLDEST,
|
PARAM_SORT_ORDER_OLDEST,
|
||||||
PARAM_SORT_TYPE_TAKEN_AT,
|
PARAM_SORT_TYPE_TAKEN_AT,
|
||||||
PARAM_SORT_TYPE_UPLOADED_AT,
|
PARAM_SORT_TYPE_UPLOADED_AT,
|
||||||
PATH_FEED,
|
PATH_FULL,
|
||||||
PATH_FEED_INFERRED,
|
PATH_FULL_INFERRED,
|
||||||
PATH_GRID,
|
PATH_GRID,
|
||||||
PATH_GRID_INFERRED,
|
PATH_GRID_INFERRED,
|
||||||
} from '@/app/paths';
|
} from '@/app/paths';
|
||||||
@ -84,11 +84,11 @@ export const getSortOptionsFromParams = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getPathSortComponents = (pathname: string) => {
|
export const getPathSortComponents = (pathname: string) => {
|
||||||
const [_, gridOrFeed, sortType, sortOrder] = pathname.split('/');
|
const [_, gridOrFull, sortType, sortOrder] = pathname.split('/');
|
||||||
return {
|
return {
|
||||||
gridOrFeed: gridOrFeed || (GRID_HOMEPAGE_ENABLED
|
gridOrFull: gridOrFull || (GRID_HOMEPAGE_ENABLED
|
||||||
? 'grid'
|
? 'grid'
|
||||||
: 'feed'
|
: 'full'
|
||||||
),
|
),
|
||||||
sortType: sortType || DEFAULT_SORT_TYPE,
|
sortType: sortType || DEFAULT_SORT_TYPE,
|
||||||
sortOrder: sortOrder || DEFAULT_SORT_ORDER,
|
sortOrder: sortOrder || DEFAULT_SORT_ORDER,
|
||||||
@ -101,7 +101,7 @@ const getReversedSortOrder = (sortOrder: string): string =>
|
|||||||
: PARAM_SORT_ORDER_OLDEST;
|
: PARAM_SORT_ORDER_OLDEST;
|
||||||
|
|
||||||
export const getSortConfigFromPath = (pathname: string) => {
|
export const getSortConfigFromPath = (pathname: string) => {
|
||||||
const { gridOrFeed, sortType, sortOrder } = getPathSortComponents(pathname);
|
const { gridOrFull, sortType, sortOrder } = getPathSortComponents(pathname);
|
||||||
const { sortBy } = _getSortOptionsFromParams(sortType, sortOrder);
|
const { sortBy } = _getSortOptionsFromParams(sortType, sortOrder);
|
||||||
const isSortedByDefault = sortBy === USER_DEFAULT_SORT_BY;
|
const isSortedByDefault = sortBy === USER_DEFAULT_SORT_BY;
|
||||||
const reversedSortOrder = getReversedSortOrder(sortOrder);
|
const reversedSortOrder = getReversedSortOrder(sortOrder);
|
||||||
@ -116,13 +116,13 @@ export const getSortConfigFromPath = (pathname: string) => {
|
|||||||
pathGrid: isSortedByDefault
|
pathGrid: isSortedByDefault
|
||||||
? PATH_GRID_INFERRED
|
? PATH_GRID_INFERRED
|
||||||
: `${PATH_GRID}/${sortType}/${sortOrder}`,
|
: `${PATH_GRID}/${sortType}/${sortOrder}`,
|
||||||
pathFeed: isSortedByDefault
|
pathFull: isSortedByDefault
|
||||||
? PATH_FEED_INFERRED
|
? PATH_FULL_INFERRED
|
||||||
: `${PATH_FEED}/${sortType}/${sortOrder}`,
|
: `${PATH_FULL}/${sortType}/${sortOrder}`,
|
||||||
pathSort: doesReverseSortMatchDefault
|
pathSort: doesReverseSortMatchDefault
|
||||||
? gridOrFeed === 'grid'
|
? gridOrFull === 'grid'
|
||||||
? PATH_GRID_INFERRED
|
? PATH_GRID_INFERRED
|
||||||
: PATH_FEED_INFERRED
|
: PATH_FULL_INFERRED
|
||||||
: `/${gridOrFeed}/${sortType}/${reversedSortOrder}`,
|
: `/${gridOrFull}/${sortType}/${reversedSortOrder}`,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,16 +2,21 @@ import { ComponentProps } from 'react';
|
|||||||
import FieldSetWithStatus from '@/components/FieldSetWithStatus';
|
import FieldSetWithStatus from '@/components/FieldSetWithStatus';
|
||||||
import IconHidden from '@/components/icons/IconHidden';
|
import IconHidden from '@/components/icons/IconHidden';
|
||||||
|
|
||||||
export default function FieldsetHidden(props: Omit<
|
export default function FieldsetExclude(props: Omit<
|
||||||
ComponentProps<typeof FieldSetWithStatus>,
|
ComponentProps<typeof FieldSetWithStatus>,
|
||||||
'label' | 'icon' | 'type'
|
'label' | 'icon' | 'type'
|
||||||
>) {
|
>) {
|
||||||
return (
|
return (
|
||||||
<FieldSetWithStatus
|
<FieldSetWithStatus
|
||||||
{...props}
|
{...props}
|
||||||
label="Hidden"
|
label="Exclude from feeds"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
icon={<IconHidden size={17} visible={props.value !== 'true'} />}
|
icon={<IconHidden
|
||||||
|
size={17}
|
||||||
|
className="translate-y-[0.5px]"
|
||||||
|
visible={props.value !== 'true'}
|
||||||
|
/>}
|
||||||
|
tooltip="Do not show on homepage views or RSS"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
22
src/photo/form/FieldsetPrivate.tsx
Normal file
22
src/photo/form/FieldsetPrivate.tsx
Normal file
@ -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<typeof FieldSetWithStatus>,
|
||||||
|
'label' | 'icon' | 'type'
|
||||||
|
>) {
|
||||||
|
return (
|
||||||
|
<FieldSetWithStatus
|
||||||
|
{...props}
|
||||||
|
label="Private"
|
||||||
|
type="checkbox"
|
||||||
|
icon={<IconLock
|
||||||
|
size={15}
|
||||||
|
open={props.value !== 'true'}
|
||||||
|
narrow
|
||||||
|
/>}
|
||||||
|
tooltip="Visible only to authenticated admin"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -45,9 +45,10 @@ import { convertFilmsForForm, Films } from '@/film';
|
|||||||
import { isMakeFujifilm } from '@/platforms/fujifilm';
|
import { isMakeFujifilm } from '@/platforms/fujifilm';
|
||||||
import PhotoFilmIcon from '@/film/PhotoFilmIcon';
|
import PhotoFilmIcon from '@/film/PhotoFilmIcon';
|
||||||
import FieldsetFavs from './FieldsetFavs';
|
import FieldsetFavs from './FieldsetFavs';
|
||||||
import FieldsetHidden from './FieldsetHidden';
|
import FieldsetPrivate from './FieldsetPrivate';
|
||||||
import { useAppText } from '@/i18n/state/client';
|
import { useAppText } from '@/i18n/state/client';
|
||||||
import IconAddUpload from '@/components/icons/IconAddUpload';
|
import IconAddUpload from '@/components/icons/IconAddUpload';
|
||||||
|
import FieldsetExclude from './FieldsetExclude';
|
||||||
|
|
||||||
const THUMBNAIL_SIZE = 300;
|
const THUMBNAIL_SIZE = 300;
|
||||||
|
|
||||||
@ -184,6 +185,16 @@ export default function PhotoForm({
|
|||||||
onTextContentChange?.(formHasTextContent(formData));
|
onTextContentChange?.(formHasTextContent(formData));
|
||||||
}, [onTextContentChange, formData]);
|
}, [onTextContentChange, formData]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (formData.hidden === 'true') {
|
||||||
|
setFormData(data => ({
|
||||||
|
...data,
|
||||||
|
excludeFromFeeds: 'false',
|
||||||
|
favorite: 'false',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}, [formData.hidden]);
|
||||||
|
|
||||||
const isFieldGeneratingAi = (key: keyof PhotoFormData) => {
|
const isFieldGeneratingAi = (key: keyof PhotoFormData) => {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case 'title':
|
case 'title':
|
||||||
@ -241,7 +252,7 @@ export default function PhotoForm({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const shouldHideField = (
|
const isFieldHidden = (
|
||||||
key: FormFields,
|
key: FormFields,
|
||||||
hideIfEmpty?: boolean,
|
hideIfEmpty?: boolean,
|
||||||
shouldHide?: FormMeta['shouldHide'],
|
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) => {
|
const onMatchResults = useCallback((didFindMatchingPhotos: boolean) => {
|
||||||
setFormData(data => ({
|
setFormData(data => ({
|
||||||
...data,
|
...data,
|
||||||
@ -361,7 +379,7 @@ export default function PhotoForm({
|
|||||||
type,
|
type,
|
||||||
staticValue,
|
staticValue,
|
||||||
}]) => {
|
}]) => {
|
||||||
if (!shouldHideField(key, hideIfEmpty, shouldHide)) {
|
if (!isFieldHidden(key, hideIfEmpty, shouldHide)) {
|
||||||
const fieldProps: ComponentProps<typeof FieldSetWithStatus> = {
|
const fieldProps: ComponentProps<typeof FieldSetWithStatus> = {
|
||||||
id: key,
|
id: key,
|
||||||
label: label + (
|
label: label + (
|
||||||
@ -403,7 +421,7 @@ export default function PhotoForm({
|
|||||||
tagOptionsLimit,
|
tagOptionsLimit,
|
||||||
tagOptionsLimitValidationMessage,
|
tagOptionsLimitValidationMessage,
|
||||||
required,
|
required,
|
||||||
readOnly,
|
readOnly: readOnly || isFieldReadOnly(key),
|
||||||
spellCheck,
|
spellCheck,
|
||||||
capitalize,
|
capitalize,
|
||||||
placeholder: loadingMessage && !formData[key]
|
placeholder: loadingMessage && !formData[key]
|
||||||
@ -445,8 +463,13 @@ export default function PhotoForm({
|
|||||||
key={key}
|
key={key}
|
||||||
{...fieldProps}
|
{...fieldProps}
|
||||||
/>;
|
/>;
|
||||||
|
case 'excludeFromFeeds':
|
||||||
|
return <FieldsetExclude
|
||||||
|
key={key}
|
||||||
|
{...fieldProps}
|
||||||
|
/>;
|
||||||
case 'hidden':
|
case 'hidden':
|
||||||
return <FieldsetHidden
|
return <FieldsetPrivate
|
||||||
key={key}
|
key={key}
|
||||||
{...fieldProps}
|
{...fieldProps}
|
||||||
/>;
|
/>;
|
||||||
@ -462,7 +485,7 @@ export default function PhotoForm({
|
|||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className={clsx(
|
<div className={clsx(
|
||||||
'flex gap-3 sticky bottom-0',
|
'flex gap-3 sticky bottom-0',
|
||||||
'pb-4 md:pb-8 mt-12',
|
'pb-4 md:pb-8 mt-16',
|
||||||
)}>
|
)}>
|
||||||
<Link
|
<Link
|
||||||
className="button"
|
className="button"
|
||||||
|
|||||||
@ -182,6 +182,7 @@ const FORM_METADATA = (
|
|||||||
},
|
},
|
||||||
priorityOrder: { label: 'priority order' },
|
priorityOrder: { label: 'priority order' },
|
||||||
favorite: { label: 'favorite', type: 'checkbox', excludeFromInsert: true },
|
favorite: { label: 'favorite', type: 'checkbox', excludeFromInsert: true },
|
||||||
|
excludeFromFeeds: { label: 'exclude from feeds', type: 'checkbox' },
|
||||||
hidden: { label: 'hidden', type: 'checkbox' },
|
hidden: { label: 'hidden', type: 'checkbox' },
|
||||||
shouldStripGpsData: {
|
shouldStripGpsData: {
|
||||||
label: 'strip gps data',
|
label: 'strip gps data',
|
||||||
@ -338,6 +339,7 @@ export const convertFormDataToPhotoDbInsert = (
|
|||||||
priorityOrder: photoForm.priorityOrder
|
priorityOrder: photoForm.priorityOrder
|
||||||
? parseFloat(photoForm.priorityOrder)
|
? parseFloat(photoForm.priorityOrder)
|
||||||
: undefined,
|
: undefined,
|
||||||
|
excludeFromFeeds: photoForm.excludeFromFeeds === 'true',
|
||||||
hidden: photoForm.hidden === 'true',
|
hidden: photoForm.hidden === 'true',
|
||||||
...generateTakenAtFields(photoForm),
|
...generateTakenAtFields(photoForm),
|
||||||
};
|
};
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import {
|
|||||||
SHOW_LENSES,
|
SHOW_LENSES,
|
||||||
SHOW_RECIPES,
|
SHOW_RECIPES,
|
||||||
} from '@/app/config';
|
} from '@/app/config';
|
||||||
import { ABSOLUTE_PATH_FOR_HOME_IMAGE } from '@/app/paths';
|
import { ABSOLUTE_PATH_HOME_IMAGE } from '@/app/paths';
|
||||||
import { formatDate, formatDateFromPostgresString } from '@/utility/date';
|
import { formatDate, formatDateFromPostgresString } from '@/utility/date';
|
||||||
import {
|
import {
|
||||||
formatAperture,
|
formatAperture,
|
||||||
@ -25,10 +25,10 @@ import { FujifilmSimulation } from '@/platforms/fujifilm/simulation';
|
|||||||
import { PhotoSyncStatus, generatePhotoSyncStatus } from './sync';
|
import { PhotoSyncStatus, generatePhotoSyncStatus } from './sync';
|
||||||
import { AppTextState } from '@/i18n/state';
|
import { AppTextState } from '@/i18n/state';
|
||||||
|
|
||||||
// INFINITE SCROLL: FEED
|
// INFINITE SCROLL: FULL
|
||||||
export const INFINITE_SCROLL_FEED_INITIAL =
|
export const INFINITE_SCROLL_FULL_INITIAL =
|
||||||
process.env.NODE_ENV === 'development' ? 2 : 12;
|
process.env.NODE_ENV === 'development' ? 2 : 12;
|
||||||
export const INFINITE_SCROLL_FEED_MULTIPLE =
|
export const INFINITE_SCROLL_FULL_MULTIPLE =
|
||||||
process.env.NODE_ENV === 'development' ? 2 : 24;
|
process.env.NODE_ENV === 'development' ? 2 : 24;
|
||||||
|
|
||||||
// INFINITE SCROLL: GRID
|
// INFINITE SCROLL: GRID
|
||||||
@ -84,6 +84,7 @@ export interface PhotoDbInsert extends PhotoExif {
|
|||||||
recipeTitle?: string
|
recipeTitle?: string
|
||||||
locationName?: string
|
locationName?: string
|
||||||
priorityOrder?: number
|
priorityOrder?: number
|
||||||
|
excludeFromFeeds?: boolean
|
||||||
hidden?: boolean
|
hidden?: boolean
|
||||||
takenAt: string
|
takenAt: string
|
||||||
takenAtNaive: string
|
takenAtNaive: string
|
||||||
@ -198,11 +199,11 @@ export const generateOgImageMetaForPhotos = (photos: Photo[]): Metadata => {
|
|||||||
if (photos.length > 0) {
|
if (photos.length > 0) {
|
||||||
return {
|
return {
|
||||||
openGraph: {
|
openGraph: {
|
||||||
images: ABSOLUTE_PATH_FOR_HOME_IMAGE,
|
images: ABSOLUTE_PATH_HOME_IMAGE,
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
card: 'summary_large_image',
|
card: 'summary_large_image',
|
||||||
images: ABSOLUTE_PATH_FOR_HOME_IMAGE,
|
images: ABSOLUTE_PATH_HOME_IMAGE,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
export const KEY_COMMANDS = {
|
export const KEY_COMMANDS = {
|
||||||
feed: 'F',
|
full: 'F',
|
||||||
grid: 'G',
|
grid: 'G',
|
||||||
admin: 'A',
|
admin: 'A',
|
||||||
prev: ['J', 'ARROWLEFT'],
|
prev: ['J', 'ARROWLEFT'],
|
||||||
@ -7,7 +7,7 @@ export const KEY_COMMANDS = {
|
|||||||
edit: 'E',
|
edit: 'E',
|
||||||
favorite: 'P',
|
favorite: 'P',
|
||||||
unfavorite: 'X',
|
unfavorite: 'X',
|
||||||
toggleHide: 'H',
|
togglePrivate: 'M',
|
||||||
download: 'D',
|
download: 'D',
|
||||||
sync: 'S',
|
sync: 'S',
|
||||||
search: ['⌘', 'K'],
|
search: ['⌘', 'K'],
|
||||||
|
|||||||
@ -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 (
|
|
||||||
<EntityLink
|
|
||||||
{...props}
|
|
||||||
label={TAG_HIDDEN}
|
|
||||||
path={pathForTag(TAG_HIDDEN)}
|
|
||||||
icon={<IconHidden size={16} />}
|
|
||||||
iconBadgeEnd={<IconHidden
|
|
||||||
size={13}
|
|
||||||
className="translate-y-[-0.5px]"
|
|
||||||
/>}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -7,7 +7,7 @@ import EntityLink, {
|
|||||||
} from '@/components/entity/EntityLink';
|
} from '@/components/entity/EntityLink';
|
||||||
import IconFavs from '@/components/icons/IconFavs';
|
import IconFavs from '@/components/icons/IconFavs';
|
||||||
|
|
||||||
export default function FavsTag(props: EntityLinkExternalProps) {
|
export default function PhotoFavs(props: EntityLinkExternalProps) {
|
||||||
return (
|
return (
|
||||||
<EntityLink
|
<EntityLink
|
||||||
{...props}
|
{...props}
|
||||||
26
src/tag/PhotoPrivate.tsx
Normal file
26
src/tag/PhotoPrivate.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { TAG_PRIVATE } from '.';
|
||||||
|
import { pathForTag } from '@/app/paths';
|
||||||
|
import EntityLink, {
|
||||||
|
EntityLinkExternalProps,
|
||||||
|
} from '@/components/entity/EntityLink';
|
||||||
|
import IconLock from '@/components/icons/IconLock';
|
||||||
|
|
||||||
|
export default function PhotoPrivate(props: EntityLinkExternalProps) {
|
||||||
|
return (
|
||||||
|
<EntityLink
|
||||||
|
{...props}
|
||||||
|
label={TAG_PRIVATE}
|
||||||
|
path={pathForTag(TAG_PRIVATE)}
|
||||||
|
icon={<IconLock
|
||||||
|
size={15}
|
||||||
|
className="translate-y-[-0.5px]"
|
||||||
|
narrow
|
||||||
|
/>}
|
||||||
|
iconBadgeEnd={<IconLock
|
||||||
|
size={8}
|
||||||
|
className="translate-y-[-0.5px]"
|
||||||
|
solid
|
||||||
|
/>}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import PhotoTag from '@/tag/PhotoTag';
|
import PhotoTag from '@/tag/PhotoTag';
|
||||||
import { isTagFavs } from '.';
|
import { isTagFavs } from '.';
|
||||||
import FavsTag from './FavsTag';
|
import PhotoFavs from './PhotoFavs';
|
||||||
import { EntityLinkExternalProps } from '@/components/entity/EntityLink';
|
import { EntityLinkExternalProps } from '@/components/entity/EntityLink';
|
||||||
import { Fragment } from 'react';
|
import { Fragment } from 'react';
|
||||||
|
|
||||||
@ -18,7 +18,7 @@ export default function PhotoTags({
|
|||||||
{tags.map(tag =>
|
{tags.map(tag =>
|
||||||
<Fragment key={tag}>
|
<Fragment key={tag}>
|
||||||
{isTagFavs(tag)
|
{isTagFavs(tag)
|
||||||
? <FavsTag {...{
|
? <PhotoFavs {...{
|
||||||
contrast,
|
contrast,
|
||||||
prefetch,
|
prefetch,
|
||||||
countOnHover: tagCounts[tag],
|
countOnHover: tagCounts[tag],
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import { Photo, photoQuantityText } from '@/photo';
|
import { Photo, photoQuantityText } from '@/photo';
|
||||||
import PhotoHeader from '@/photo/PhotoHeader';
|
import PhotoHeader from '@/photo/PhotoHeader';
|
||||||
import HiddenTag from './HiddenTag';
|
import PhotoPrivate from './PhotoPrivate';
|
||||||
import { AI_TEXT_GENERATION_ENABLED } from '@/app/config';
|
import { AI_TEXT_GENERATION_ENABLED } from '@/app/config';
|
||||||
import { getAppText } from '@/i18n/state/server';
|
import { getAppText } from '@/i18n/state/server';
|
||||||
|
|
||||||
export default async function HiddenHeader({
|
export default async function PrivateHeader({
|
||||||
photos,
|
photos,
|
||||||
selectedPhoto,
|
selectedPhoto,
|
||||||
indexNumber,
|
indexNumber,
|
||||||
@ -19,7 +19,7 @@ export default async function HiddenHeader({
|
|||||||
return (
|
return (
|
||||||
<PhotoHeader
|
<PhotoHeader
|
||||||
key="HiddenHeader"
|
key="HiddenHeader"
|
||||||
entity={<HiddenTag contrast="high" />}
|
entity={<PhotoPrivate contrast="high" />}
|
||||||
entityDescription={photoQuantityText(count, appText, false, false)}
|
entityDescription={photoQuantityText(count, appText, false, false)}
|
||||||
photos={photos}
|
photos={photos}
|
||||||
selectedPhoto={selectedPhoto}
|
selectedPhoto={selectedPhoto}
|
||||||
@ -2,7 +2,7 @@ import { Photo, PhotoDateRange } from '@/photo';
|
|||||||
import PhotoTag from './PhotoTag';
|
import PhotoTag from './PhotoTag';
|
||||||
import { descriptionForTaggedPhotos, isTagFavs } from '.';
|
import { descriptionForTaggedPhotos, isTagFavs } from '.';
|
||||||
import PhotoHeader from '@/photo/PhotoHeader';
|
import PhotoHeader from '@/photo/PhotoHeader';
|
||||||
import FavsTag from './FavsTag';
|
import PhotoFavs from './PhotoFavs';
|
||||||
import { AI_TEXT_GENERATION_ENABLED } from '@/app/config';
|
import { AI_TEXT_GENERATION_ENABLED } from '@/app/config';
|
||||||
import { getAppText } from '@/i18n/state/server';
|
import { getAppText } from '@/i18n/state/server';
|
||||||
|
|
||||||
@ -26,7 +26,7 @@ export default async function TagHeader({
|
|||||||
<PhotoHeader
|
<PhotoHeader
|
||||||
tag={tag}
|
tag={tag}
|
||||||
entity={isTagFavs(tag)
|
entity={isTagFavs(tag)
|
||||||
? <FavsTag
|
? <PhotoFavs
|
||||||
contrast="high"
|
contrast="high"
|
||||||
showHover={false}
|
showHover={false}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -20,7 +20,7 @@ import { AppTextState } from '@/i18n/state';
|
|||||||
|
|
||||||
// Reserved tags
|
// Reserved tags
|
||||||
export const TAG_FAVS = 'favs';
|
export const TAG_FAVS = 'favs';
|
||||||
export const TAG_HIDDEN = 'hidden';
|
export const TAG_PRIVATE = 'private';
|
||||||
|
|
||||||
type TagWithMeta = { tag: string } & CategoryQueryMeta;
|
type TagWithMeta = { tag: string } & CategoryQueryMeta;
|
||||||
|
|
||||||
@ -31,7 +31,7 @@ export const formatTag = (tag?: string) =>
|
|||||||
|
|
||||||
export const getValidationMessageForTags = (tags?: string) => {
|
export const getValidationMessageForTags = (tags?: string) => {
|
||||||
const reservedTags = (convertStringToArray(tags) ?? [])
|
const reservedTags = (convertStringToArray(tags) ?? [])
|
||||||
.filter(tag => isTagFavs(tag) || isTagHidden(tag))
|
.filter(tag => isTagFavs(tag) || isTagPrivate(tag))
|
||||||
.map(tag => tag.toLocaleUpperCase());
|
.map(tag => tag.toLocaleUpperCase());
|
||||||
return reservedTags.length
|
return reservedTags.length
|
||||||
? `Reserved tags: ${reservedTags.join(', ').toLocaleLowerCase()}`
|
? `Reserved tags: ${reservedTags.join(', ').toLocaleLowerCase()}`
|
||||||
@ -135,20 +135,20 @@ export const isPhotoFav = ({ tags }: Photo) => tags.some(isTagFavs);
|
|||||||
export const isPathFavs = (pathname?: string) =>
|
export const isPathFavs = (pathname?: string) =>
|
||||||
getPathComponents(pathname).tag === TAG_FAVS;
|
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,
|
tags: Tags,
|
||||||
countHidden = 0,
|
countPrivate = 0,
|
||||||
lastModifiedHidden = new Date(),
|
lastModifiedPrivate = new Date(),
|
||||||
) =>
|
) =>
|
||||||
countHidden > 0
|
countPrivate > 0
|
||||||
? tags
|
? tags
|
||||||
.filter(({ tag }) => tag === TAG_FAVS)
|
.filter(({ tag }) => tag === TAG_FAVS)
|
||||||
.concat({
|
.concat({
|
||||||
tag: TAG_HIDDEN,
|
tag: TAG_PRIVATE,
|
||||||
count: countHidden,
|
count: countPrivate,
|
||||||
lastModified: lastModifiedHidden,
|
lastModified: lastModifiedPrivate,
|
||||||
})
|
})
|
||||||
.concat(tags
|
.concat(tags
|
||||||
.filter(({ tag }) => tag !== TAG_FAVS)
|
.filter(({ tag }) => tag !== TAG_FAVS)
|
||||||
@ -176,7 +176,7 @@ export const limitTagsByCount = (
|
|||||||
tags.filter(({ tag, count }) => (
|
tags.filter(({ tag, count }) => (
|
||||||
count >= minimumCount ||
|
count >= minimumCount ||
|
||||||
isTagFavs(tag) ||
|
isTagFavs(tag) ||
|
||||||
isTagHidden(tag) ||
|
isTagPrivate(tag) ||
|
||||||
(queryToInclude && tag
|
(queryToInclude && tag
|
||||||
.toLocaleLowerCase()
|
.toLocaleLowerCase()
|
||||||
.includes(queryToInclude.toLocaleLowerCase()))
|
.includes(queryToInclude.toLocaleLowerCase()))
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user