Lazy load language data
This commit is contained in:
parent
878edc713d
commit
526ba1a43b
@ -8,9 +8,9 @@ import { cache } from 'react';
|
|||||||
import { PATH_ROOT } from '@/app/paths';
|
import { PATH_ROOT } from '@/app/paths';
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
import { staticallyGenerateCategoryIfConfigured } from '@/app/static';
|
import { staticallyGenerateCategoryIfConfigured } from '@/app/static';
|
||||||
|
import { getAppText } from '@/i18n/state/server';
|
||||||
|
|
||||||
const getPhotosFilmDataCachedCached =
|
const getPhotosFilmDataCachedCached = cache(getPhotosFilmDataCached);
|
||||||
cache(getPhotosFilmDataCached);
|
|
||||||
|
|
||||||
export const generateStaticParams = staticallyGenerateCategoryIfConfigured(
|
export const generateStaticParams = staticallyGenerateCategoryIfConfigured(
|
||||||
'films',
|
'films',
|
||||||
@ -38,12 +38,14 @@ export async function generateMetadata({
|
|||||||
|
|
||||||
if (photos.length === 0) { return {}; }
|
if (photos.length === 0) { return {}; }
|
||||||
|
|
||||||
|
const appText = await getAppText();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
url,
|
url,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
images,
|
images,
|
||||||
} = generateMetaForFilm(film, photos, count, dateRange);
|
} = generateMetaForFilm(film, photos, appText, count, dateRange);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import type { Metadata } from 'next';
|
|||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
import { cache } from 'react';
|
import { cache } from 'react';
|
||||||
import { staticallyGenerateCategoryIfConfigured } from '@/app/static';
|
import { staticallyGenerateCategoryIfConfigured } from '@/app/static';
|
||||||
|
import { getAppText } from '@/i18n/state/server';
|
||||||
|
|
||||||
const getPhotosFocalDataCachedCached = cache((focal: number) =>
|
const getPhotosFocalDataCachedCached = cache((focal: number) =>
|
||||||
getPhotosFocalLengthDataCached({
|
getPhotosFocalLengthDataCached({
|
||||||
@ -41,12 +42,14 @@ export async function generateMetadata({
|
|||||||
|
|
||||||
if (photos.length === 0) { return {}; }
|
if (photos.length === 0) { return {}; }
|
||||||
|
|
||||||
|
const appText = await getAppText();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
url,
|
url,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
images,
|
images,
|
||||||
} = generateMetaForFocalLength(focal, photos, count, dateRange);
|
} = generateMetaForFocalLength(focal, photos, appText, count, dateRange);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
|
|||||||
@ -24,6 +24,7 @@ import AdminUploadPanel from '@/admin/upload/AdminUploadPanel';
|
|||||||
import { revalidatePath } from 'next/cache';
|
import { revalidatePath } from 'next/cache';
|
||||||
import RecipeModal from '@/recipe/RecipeModal';
|
import RecipeModal from '@/recipe/RecipeModal';
|
||||||
import ThemeColors from '@/app/ThemeColors';
|
import ThemeColors from '@/app/ThemeColors';
|
||||||
|
import AppTextProvider from '@/i18n/state/AppTextProvider';
|
||||||
|
|
||||||
import '../tailwind.css';
|
import '../tailwind.css';
|
||||||
|
|
||||||
@ -80,6 +81,7 @@ export default function RootLayout({
|
|||||||
'3xl:flex flex-col items-center',
|
'3xl:flex flex-col items-center',
|
||||||
)}>
|
)}>
|
||||||
<AppStateProvider>
|
<AppStateProvider>
|
||||||
|
<AppTextProvider>
|
||||||
<ThemeColors />
|
<ThemeColors />
|
||||||
<ThemeProvider attribute="class" defaultTheme={DEFAULT_THEME}>
|
<ThemeProvider attribute="class" defaultTheme={DEFAULT_THEME}>
|
||||||
<SwrConfigClient>
|
<SwrConfigClient>
|
||||||
@ -117,6 +119,7 @@ export default function RootLayout({
|
|||||||
<PhotoEscapeHandler />
|
<PhotoEscapeHandler />
|
||||||
<ToasterWithThemes />
|
<ToasterWithThemes />
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
</AppTextProvider>
|
||||||
</AppStateProvider>
|
</AppStateProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
staticallyGenerateCategoryIfConfigured,
|
staticallyGenerateCategoryIfConfigured,
|
||||||
} from '@/app/static';
|
} from '@/app/static';
|
||||||
|
import { getAppText } from '@/i18n/state/server';
|
||||||
|
|
||||||
const getPhotosLensDataCachedCached = cache((
|
const getPhotosLensDataCachedCached = cache((
|
||||||
make: string | undefined,
|
make: string | undefined,
|
||||||
@ -41,12 +42,14 @@ export async function generateMetadata({
|
|||||||
lens,
|
lens,
|
||||||
] = await getPhotosLensDataCachedCached(make, model);
|
] = await getPhotosLensDataCachedCached(make, model);
|
||||||
|
|
||||||
|
const appText = await getAppText();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
url,
|
url,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
images,
|
images,
|
||||||
} = generateMetaForLens(lens, photos, count, dateRange);
|
} = generateMetaForLens(lens, photos, appText, count, dateRange);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { generateMetaForRecipe } from '@/recipe';
|
|||||||
import RecipeOverview from '@/recipe/RecipeOverview';
|
import RecipeOverview from '@/recipe/RecipeOverview';
|
||||||
import { getPhotosRecipeDataCached } from '@/recipe/data';
|
import { getPhotosRecipeDataCached } from '@/recipe/data';
|
||||||
import { staticallyGenerateCategoryIfConfigured } from '@/app/static';
|
import { staticallyGenerateCategoryIfConfigured } from '@/app/static';
|
||||||
|
import { getAppText } from '@/i18n/state/server';
|
||||||
|
|
||||||
const getPhotosRecipeDataCachedCached = cache(getPhotosRecipeDataCached);
|
const getPhotosRecipeDataCachedCached = cache(getPhotosRecipeDataCached);
|
||||||
|
|
||||||
@ -39,12 +40,14 @@ export async function generateMetadata({
|
|||||||
|
|
||||||
if (photos.length === 0) { return {}; }
|
if (photos.length === 0) { return {}; }
|
||||||
|
|
||||||
|
const appText = await getAppText();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
url,
|
url,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
images,
|
images,
|
||||||
} = generateMetaForRecipe(recipe, photos, count, dateRange);
|
} = generateMetaForRecipe(recipe, photos, appText, count, dateRange);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import CameraOverview from '@/camera/CameraOverview';
|
|||||||
import { cache } from 'react';
|
import { cache } from 'react';
|
||||||
import { getUniqueCameras } from '@/photo/db/query';
|
import { getUniqueCameras } from '@/photo/db/query';
|
||||||
import { staticallyGenerateCategoryIfConfigured } from '@/app/static';
|
import { staticallyGenerateCategoryIfConfigured } from '@/app/static';
|
||||||
|
import { getAppText } from '@/i18n/state/server';
|
||||||
|
|
||||||
const getPhotosCameraDataCachedCached = cache((
|
const getPhotosCameraDataCachedCached = cache((
|
||||||
make: string,
|
make: string,
|
||||||
@ -35,12 +36,14 @@ export async function generateMetadata({
|
|||||||
camera,
|
camera,
|
||||||
] = await getPhotosCameraDataCachedCached(make, model);
|
] = await getPhotosCameraDataCachedCached(make, model);
|
||||||
|
|
||||||
|
const appText = await getAppText();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
url,
|
url,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
images,
|
images,
|
||||||
} = generateMetaForCamera(camera, photos, count, dateRange);
|
} = generateMetaForCamera(camera, photos, appText, count, dateRange);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { clsx } from 'clsx/lite';
|
|||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
import LinkWithStatus from '@/components/LinkWithStatus';
|
import LinkWithStatus from '@/components/LinkWithStatus';
|
||||||
import { IoArrowBack } from 'react-icons/io5';
|
import { IoArrowBack } from 'react-icons/io5';
|
||||||
import { APP_TEXT } from '@/app/config';
|
import { getAppText } from '@/i18n/state/server';
|
||||||
|
|
||||||
export default async function SignInPage() {
|
export default async function SignInPage() {
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
@ -14,6 +14,8 @@ export default async function SignInPage() {
|
|||||||
redirect(PATH_ADMIN);
|
redirect(PATH_ADMIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const appText = await getAppText();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx(
|
<div className={clsx(
|
||||||
'fixed top-0 left-0 right-0 bottom-0',
|
'fixed top-0 left-0 right-0 bottom-0',
|
||||||
@ -28,7 +30,7 @@ export default async function SignInPage() {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<IoArrowBack className="translate-y-[1px]" />
|
<IoArrowBack className="translate-y-[1px]" />
|
||||||
{APP_TEXT.nav.home}
|
{appText.nav.home}
|
||||||
</LinkWithStatus>
|
</LinkWithStatus>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import type { Metadata } from 'next';
|
|||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
import { cache } from 'react';
|
import { cache } from 'react';
|
||||||
import { staticallyGenerateCategoryIfConfigured } from '@/app/static';
|
import { staticallyGenerateCategoryIfConfigured } from '@/app/static';
|
||||||
|
import { getAppText } from '@/i18n/state/server';
|
||||||
|
|
||||||
const getPhotosTagDataCachedCached = cache((tag: string) =>
|
const getPhotosTagDataCachedCached = cache((tag: string) =>
|
||||||
getPhotosTagDataCached({ tag, limit: INFINITE_SCROLL_GRID_INITIAL}));
|
getPhotosTagDataCached({ tag, limit: INFINITE_SCROLL_GRID_INITIAL}));
|
||||||
@ -37,12 +38,14 @@ export async function generateMetadata({
|
|||||||
|
|
||||||
if (photos.length === 0) { return {}; }
|
if (photos.length === 0) { return {}; }
|
||||||
|
|
||||||
|
const appText = await getAppText();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
url,
|
url,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
images,
|
images,
|
||||||
} = generateMetaForTag(tag, photos, count, dateRange);
|
} = generateMetaForTag(tag, photos, appText, count, dateRange);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { TAG_HIDDEN, descriptionForTaggedPhotos, titleForTag } from '@/tag';
|
|||||||
import HiddenHeader from '@/tag/HiddenHeader';
|
import HiddenHeader from '@/tag/HiddenHeader';
|
||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next';
|
||||||
import { cache } from 'react';
|
import { cache } from 'react';
|
||||||
|
import { getAppText } from '@/i18n/state/server';
|
||||||
|
|
||||||
const getPhotosHiddenMetaCached = cache(() =>
|
const getPhotosHiddenMetaCached = cache(() =>
|
||||||
getPhotosMetaCached({ hidden: 'only' }));
|
getPhotosMetaCached({ hidden: 'only' }));
|
||||||
@ -17,9 +18,13 @@ export async function generateMetadata(): Promise<Metadata> {
|
|||||||
|
|
||||||
if (count === 0) { return {}; }
|
if (count === 0) { return {}; }
|
||||||
|
|
||||||
const title = titleForTag(TAG_HIDDEN, undefined, count);
|
const appText = await getAppText();
|
||||||
|
|
||||||
|
const title = titleForTag(TAG_HIDDEN, undefined, appText, count);
|
||||||
|
|
||||||
const description = descriptionForTaggedPhotos(
|
const description = descriptionForTaggedPhotos(
|
||||||
undefined,
|
undefined,
|
||||||
|
appText,
|
||||||
undefined,
|
undefined,
|
||||||
count,
|
count,
|
||||||
dateRange,
|
dateRange,
|
||||||
|
|||||||
@ -29,7 +29,7 @@ import IconBroom from '@/components/icons/IconBroom';
|
|||||||
import InsightsIndicatorDot from './insights/InsightsIndicatorDot';
|
import InsightsIndicatorDot from './insights/InsightsIndicatorDot';
|
||||||
import MoreMenuItem from '@/components/more/MoreMenuItem';
|
import MoreMenuItem from '@/components/more/MoreMenuItem';
|
||||||
import Spinner from '@/components/Spinner';
|
import Spinner from '@/components/Spinner';
|
||||||
import { APP_TEXT } from '@/app/config';
|
import { useAppText } from '@/i18n/state/client';
|
||||||
|
|
||||||
export default function AdminAppMenu({
|
export default function AdminAppMenu({
|
||||||
active,
|
active,
|
||||||
@ -58,6 +58,8 @@ export default function AdminAppMenu({
|
|||||||
clearAuthStateAndRedirectIfNecessary,
|
clearAuthStateAndRedirectIfNecessary,
|
||||||
} = useAppState();
|
} = useAppState();
|
||||||
|
|
||||||
|
const appText = useAppText();
|
||||||
|
|
||||||
const isSelecting = selectedPhotoIds !== undefined;
|
const isSelecting = selectedPhotoIds !== undefined;
|
||||||
|
|
||||||
const isAltPressed = useIsKeyBeingPressed('alt');
|
const isAltPressed = useIsKeyBeingPressed('alt');
|
||||||
@ -66,7 +68,7 @@ export default function AdminAppMenu({
|
|||||||
|
|
||||||
const sectionUpload: ComponentProps<typeof MoreMenuItem>[] =
|
const sectionUpload: ComponentProps<typeof MoreMenuItem>[] =
|
||||||
useMemo(() => ([{
|
useMemo(() => ([{
|
||||||
label: APP_TEXT.admin.uploadPhotos,
|
label: appText.admin.uploadPhotos,
|
||||||
icon: <IconUpload
|
icon: <IconUpload
|
||||||
size={15}
|
size={15}
|
||||||
className="translate-x-[0.5px] translate-y-[0.5px]"
|
className="translate-x-[0.5px] translate-y-[0.5px]"
|
||||||
@ -74,14 +76,14 @@ export default function AdminAppMenu({
|
|||||||
annotation: isLoadingAdminData &&
|
annotation: isLoadingAdminData &&
|
||||||
<Spinner className="translate-y-[1.5px]" />,
|
<Spinner className="translate-y-[1.5px]" />,
|
||||||
action: startUpload,
|
action: startUpload,
|
||||||
}]), [isLoadingAdminData, startUpload]);
|
}]), [appText, isLoadingAdminData, startUpload]);
|
||||||
|
|
||||||
const sectionMain: ComponentProps<typeof MoreMenuItem>[] = useMemo(() => {
|
const sectionMain: ComponentProps<typeof MoreMenuItem>[] = useMemo(() => {
|
||||||
const items: ComponentProps<typeof MoreMenuItem>[] = [];
|
const items: ComponentProps<typeof MoreMenuItem>[] = [];
|
||||||
|
|
||||||
if (uploadsCount) {
|
if (uploadsCount) {
|
||||||
items.push({
|
items.push({
|
||||||
label: APP_TEXT.admin.uploadPlural,
|
label: appText.admin.uploadPlural,
|
||||||
annotation: `${uploadsCount}`,
|
annotation: `${uploadsCount}`,
|
||||||
icon: <IconFolder
|
icon: <IconFolder
|
||||||
size={16}
|
size={16}
|
||||||
@ -92,7 +94,7 @@ export default function AdminAppMenu({
|
|||||||
}
|
}
|
||||||
if (photosCountNeedSync) {
|
if (photosCountNeedSync) {
|
||||||
items.push({
|
items.push({
|
||||||
label: APP_TEXT.admin.updatePlural,
|
label: appText.admin.updatePlural,
|
||||||
annotation: <>
|
annotation: <>
|
||||||
<span className="mr-3">
|
<span className="mr-3">
|
||||||
{photosCountNeedSync}
|
{photosCountNeedSync}
|
||||||
@ -112,7 +114,7 @@ export default function AdminAppMenu({
|
|||||||
}
|
}
|
||||||
if (photosCountTotal) {
|
if (photosCountTotal) {
|
||||||
items.push({
|
items.push({
|
||||||
label: APP_TEXT.admin.managePhotos,
|
label: appText.admin.managePhotos,
|
||||||
...photosCountTotal && {
|
...photosCountTotal && {
|
||||||
annotation: `${photosCountTotal}`,
|
annotation: `${photosCountTotal}`,
|
||||||
},
|
},
|
||||||
@ -125,7 +127,7 @@ export default function AdminAppMenu({
|
|||||||
}
|
}
|
||||||
if (tagsCount) {
|
if (tagsCount) {
|
||||||
items.push({
|
items.push({
|
||||||
label: APP_TEXT.admin.manageTags,
|
label: appText.admin.manageTags,
|
||||||
annotation: `${tagsCount}`,
|
annotation: `${tagsCount}`,
|
||||||
icon: <IconTag
|
icon: <IconTag
|
||||||
size={15}
|
size={15}
|
||||||
@ -136,7 +138,7 @@ export default function AdminAppMenu({
|
|||||||
}
|
}
|
||||||
if (recipesCount) {
|
if (recipesCount) {
|
||||||
items.push({
|
items.push({
|
||||||
label: APP_TEXT.admin.manageRecipes,
|
label: appText.admin.manageRecipes,
|
||||||
annotation: `${recipesCount}`,
|
annotation: `${recipesCount}`,
|
||||||
icon: <IconRecipe
|
icon: <IconRecipe
|
||||||
size={17}
|
size={17}
|
||||||
@ -148,8 +150,8 @@ export default function AdminAppMenu({
|
|||||||
if (photosCountTotal) {
|
if (photosCountTotal) {
|
||||||
items.push({
|
items.push({
|
||||||
label: isSelecting
|
label: isSelecting
|
||||||
? APP_TEXT.admin.batchExitEdit
|
? appText.admin.batchExitEdit
|
||||||
: APP_TEXT.admin.batchEditShort,
|
: appText.admin.batchEditShort,
|
||||||
icon: isSelecting
|
icon: isSelecting
|
||||||
? <IoCloseSharp
|
? <IoCloseSharp
|
||||||
size={18}
|
size={18}
|
||||||
@ -175,8 +177,8 @@ export default function AdminAppMenu({
|
|||||||
}
|
}
|
||||||
items.push({
|
items.push({
|
||||||
label: showAppInsightsLink
|
label: showAppInsightsLink
|
||||||
? APP_TEXT.admin.appInsights
|
? appText.admin.appInsights
|
||||||
: APP_TEXT.admin.appConfig,
|
: appText.admin.appConfig,
|
||||||
icon: <AdminAppInfoIcon
|
icon: <AdminAppInfoIcon
|
||||||
size="small"
|
size="small"
|
||||||
className="translate-x-[-0.5px] translate-y-[0.5px]"
|
className="translate-x-[-0.5px] translate-y-[0.5px]"
|
||||||
@ -188,6 +190,7 @@ export default function AdminAppMenu({
|
|||||||
|
|
||||||
return items;
|
return items;
|
||||||
}, [
|
}, [
|
||||||
|
appText,
|
||||||
isSelecting,
|
isSelecting,
|
||||||
photosCountNeedSync,
|
photosCountNeedSync,
|
||||||
photosCountTotal,
|
photosCountTotal,
|
||||||
@ -200,10 +203,10 @@ export default function AdminAppMenu({
|
|||||||
|
|
||||||
const sectionSignOut: ComponentProps<typeof MoreMenuItem>[] =
|
const sectionSignOut: ComponentProps<typeof MoreMenuItem>[] =
|
||||||
useMemo(() => ([{
|
useMemo(() => ([{
|
||||||
label: APP_TEXT.auth.signOut,
|
label: appText.auth.signOut,
|
||||||
icon: <IconSignOut size={15} />,
|
icon: <IconSignOut size={15} />,
|
||||||
action: () => signOutAction().then(clearAuthStateAndRedirectIfNecessary),
|
action: () => signOutAction().then(clearAuthStateAndRedirectIfNecessary),
|
||||||
}]), [clearAuthStateAndRedirectIfNecessary]);
|
}]), [appText.auth.signOut, clearAuthStateAndRedirectIfNecessary]);
|
||||||
|
|
||||||
const sections = useMemo(() =>
|
const sections = useMemo(() =>
|
||||||
[sectionUpload, sectionMain, sectionSignOut]
|
[sectionUpload, sectionMain, sectionSignOut]
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import { FaArrowDown, FaCheck } from 'react-icons/fa6';
|
|||||||
import ResponsiveText from '@/components/primitives/ResponsiveText';
|
import ResponsiveText from '@/components/primitives/ResponsiveText';
|
||||||
import IconFavs from '@/components/icons/IconFavs';
|
import IconFavs from '@/components/icons/IconFavs';
|
||||||
import IconTag from '@/components/icons/IconTag';
|
import IconTag from '@/components/icons/IconTag';
|
||||||
|
import { useAppText } from '@/i18n/state/client';
|
||||||
|
|
||||||
export default function AdminBatchEditPanelClient({
|
export default function AdminBatchEditPanelClient({
|
||||||
uniqueTags,
|
uniqueTags,
|
||||||
@ -37,6 +38,8 @@ export default function AdminBatchEditPanelClient({
|
|||||||
setIsPerformingSelectEdit,
|
setIsPerformingSelectEdit,
|
||||||
} = useAppState();
|
} = useAppState();
|
||||||
|
|
||||||
|
const appText = useAppText();
|
||||||
|
|
||||||
const [tags, setTags] = useState<string>();
|
const [tags, setTags] = useState<string>();
|
||||||
const [tagErrorMessage, setTagErrorMessage] = useState('');
|
const [tagErrorMessage, setTagErrorMessage] = useState('');
|
||||||
const isInTagMode = tags !== undefined;
|
const isInTagMode = tags !== undefined;
|
||||||
@ -49,6 +52,7 @@ export default function AdminBatchEditPanelClient({
|
|||||||
|
|
||||||
const photosText = photoQuantityText(
|
const photosText = photoQuantityText(
|
||||||
selectedPhotoIds?.length ?? 0,
|
selectedPhotoIds?.length ?? 0,
|
||||||
|
appText,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import {
|
|||||||
PATH_ADMIN_UPLOADS,
|
PATH_ADMIN_UPLOADS,
|
||||||
} from '@/app/paths';
|
} from '@/app/paths';
|
||||||
import AdminNavClient from './AdminNavClient';
|
import AdminNavClient from './AdminNavClient';
|
||||||
import { APP_TEXT } from '@/app/config';
|
import { getAppText } from '@/i18n/state/server';
|
||||||
|
|
||||||
export default async function AdminNav() {
|
export default async function AdminNav() {
|
||||||
const [
|
const [
|
||||||
@ -38,32 +38,34 @@ export default async function AdminNav() {
|
|||||||
getPhotosMostRecentUpdateCached().catch(() => undefined),
|
getPhotosMostRecentUpdateCached().catch(() => undefined),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const appText = await getAppText();
|
||||||
|
|
||||||
const includeInsights = countPhotos > 0;
|
const includeInsights = countPhotos > 0;
|
||||||
|
|
||||||
// Photos
|
// Photos
|
||||||
const items = [{
|
const items = [{
|
||||||
label: APP_TEXT.photo.photoPlural,
|
label: appText.photo.photoPlural,
|
||||||
href: PATH_ADMIN_PHOTOS,
|
href: PATH_ADMIN_PHOTOS,
|
||||||
count: countPhotos,
|
count: countPhotos,
|
||||||
}];
|
}];
|
||||||
|
|
||||||
// Uploads
|
// Uploads
|
||||||
if (countUploads > 0) { items.push({
|
if (countUploads > 0) { items.push({
|
||||||
label: APP_TEXT.admin.uploadPlural,
|
label: appText.admin.uploadPlural,
|
||||||
href: PATH_ADMIN_UPLOADS,
|
href: PATH_ADMIN_UPLOADS,
|
||||||
count: countUploads,
|
count: countUploads,
|
||||||
}); }
|
}); }
|
||||||
|
|
||||||
// Tags
|
// Tags
|
||||||
if (countTags > 0) { items.push({
|
if (countTags > 0) { items.push({
|
||||||
label: APP_TEXT.category.tagPlural,
|
label: appText.category.tagPlural,
|
||||||
href: PATH_ADMIN_TAGS,
|
href: PATH_ADMIN_TAGS,
|
||||||
count: countTags,
|
count: countTags,
|
||||||
}); }
|
}); }
|
||||||
|
|
||||||
// Recipes
|
// Recipes
|
||||||
if (countRecipes > 0) { items.push({
|
if (countRecipes > 0) { items.push({
|
||||||
label: APP_TEXT.category.recipePlural,
|
label: appText.category.recipePlural,
|
||||||
href: PATH_ADMIN_RECIPES,
|
href: PATH_ADMIN_RECIPES,
|
||||||
count: countRecipes,
|
count: countRecipes,
|
||||||
}); }
|
}); }
|
||||||
|
|||||||
@ -26,7 +26,7 @@ import IconFavs from '@/components/icons/IconFavs';
|
|||||||
import IconEdit from '@/components/icons/IconEdit';
|
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 { APP_TEXT } from '@/app/config';
|
import { useAppText } from '@/i18n/state/client';
|
||||||
|
|
||||||
export default function AdminPhotoMenu({
|
export default function AdminPhotoMenu({
|
||||||
photo,
|
photo,
|
||||||
@ -42,6 +42,8 @@ export default function AdminPhotoMenu({
|
|||||||
}) {
|
}) {
|
||||||
const { isUserSignedIn, registerAdminUpdate } = useAppState();
|
const { isUserSignedIn, registerAdminUpdate } = useAppState();
|
||||||
|
|
||||||
|
const appText = useAppText();
|
||||||
|
|
||||||
const isFav = isPhotoFav(photo);
|
const isFav = isPhotoFav(photo);
|
||||||
const path = usePathname();
|
const path = usePathname();
|
||||||
const shouldRedirectFav = isPathFavs(path) && isFav;
|
const shouldRedirectFav = isPathFavs(path) && isFav;
|
||||||
@ -49,7 +51,7 @@ export default function AdminPhotoMenu({
|
|||||||
|
|
||||||
const sectionMain = useMemo(() => {
|
const sectionMain = useMemo(() => {
|
||||||
const items: ComponentProps<typeof MoreMenuItem>[] = [{
|
const items: ComponentProps<typeof MoreMenuItem>[] = [{
|
||||||
label: APP_TEXT.admin.edit,
|
label: appText.admin.edit,
|
||||||
icon: <IconEdit
|
icon: <IconEdit
|
||||||
size={15}
|
size={15}
|
||||||
className="translate-x-[0.5px]"
|
className="translate-x-[0.5px]"
|
||||||
@ -59,7 +61,7 @@ export default function AdminPhotoMenu({
|
|||||||
}];
|
}];
|
||||||
if (includeFavorite) {
|
if (includeFavorite) {
|
||||||
items.push({
|
items.push({
|
||||||
label: isFav ? APP_TEXT.admin.unfavorite : APP_TEXT.admin.favorite,
|
label: isFav ? appText.admin.unfavorite : appText.admin.favorite,
|
||||||
icon: <IconFavs
|
icon: <IconFavs
|
||||||
size={14}
|
size={14}
|
||||||
className="translate-x-[-1px] translate-y-[0.5px]"
|
className="translate-x-[-1px] translate-y-[0.5px]"
|
||||||
@ -77,7 +79,7 @@ export default function AdminPhotoMenu({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
items.push({
|
items.push({
|
||||||
label: APP_TEXT.admin.download,
|
label: appText.admin.download,
|
||||||
icon: <MdOutlineFileDownload
|
icon: <MdOutlineFileDownload
|
||||||
size={17}
|
size={17}
|
||||||
className="translate-x-[-1px]"
|
className="translate-x-[-1px]"
|
||||||
@ -87,9 +89,9 @@ export default function AdminPhotoMenu({
|
|||||||
...showKeyCommands && { keyCommand: KEY_COMMANDS.download },
|
...showKeyCommands && { keyCommand: KEY_COMMANDS.download },
|
||||||
});
|
});
|
||||||
items.push({
|
items.push({
|
||||||
label: APP_TEXT.admin.sync,
|
label: appText.admin.sync,
|
||||||
labelComplex: <span className="inline-flex items-center gap-2">
|
labelComplex: <span className="inline-flex items-center gap-2">
|
||||||
<span>{APP_TEXT.admin.sync}</span>
|
<span>{appText.admin.sync}</span>
|
||||||
{photoNeedsToBeSynced(photo) &&
|
{photoNeedsToBeSynced(photo) &&
|
||||||
<InsightsIndicatorDot
|
<InsightsIndicatorDot
|
||||||
colorOverride="blue"
|
colorOverride="blue"
|
||||||
@ -107,6 +109,7 @@ export default function AdminPhotoMenu({
|
|||||||
|
|
||||||
return items;
|
return items;
|
||||||
}, [
|
}, [
|
||||||
|
appText,
|
||||||
photo,
|
photo,
|
||||||
showKeyCommands,
|
showKeyCommands,
|
||||||
includeFavorite,
|
includeFavorite,
|
||||||
@ -116,7 +119,7 @@ export default function AdminPhotoMenu({
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const sectionDelete: ComponentProps<typeof MoreMenuItem>[] = useMemo(() => [{
|
const sectionDelete: ComponentProps<typeof MoreMenuItem>[] = useMemo(() => [{
|
||||||
label: APP_TEXT.admin.delete,
|
label: appText.admin.delete,
|
||||||
icon: <BiTrash
|
icon: <BiTrash
|
||||||
size={15}
|
size={15}
|
||||||
className="translate-x-[-1px]"
|
className="translate-x-[-1px]"
|
||||||
@ -124,7 +127,7 @@ export default function AdminPhotoMenu({
|
|||||||
className: 'text-error *:hover:text-error',
|
className: 'text-error *:hover:text-error',
|
||||||
color: 'red',
|
color: 'red',
|
||||||
action: () => {
|
action: () => {
|
||||||
if (confirm(deleteConfirmationTextForPhoto(photo))) {
|
if (confirm(deleteConfirmationTextForPhoto(photo, appText))) {
|
||||||
return deletePhotoAction(
|
return deletePhotoAction(
|
||||||
photo.id,
|
photo.id,
|
||||||
photo.url,
|
photo.url,
|
||||||
@ -140,6 +143,7 @@ export default function AdminPhotoMenu({
|
|||||||
keyCommand: KEY_COMMANDS.delete[1],
|
keyCommand: KEY_COMMANDS.delete[1],
|
||||||
},
|
},
|
||||||
}], [
|
}], [
|
||||||
|
appText,
|
||||||
photo,
|
photo,
|
||||||
showKeyCommands,
|
showKeyCommands,
|
||||||
revalidatePhoto,
|
revalidatePhoto,
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import PhotoUploadWithStatus from '@/photo/PhotoUploadWithStatus';
|
|||||||
import { pluralize } from '@/utility/string';
|
import { pluralize } from '@/utility/string';
|
||||||
import IconBroom from '@/components/icons/IconBroom';
|
import IconBroom from '@/components/icons/IconBroom';
|
||||||
import ResponsiveText from '@/components/primitives/ResponsiveText';
|
import ResponsiveText from '@/components/primitives/ResponsiveText';
|
||||||
import { APP_TEXT } from '@/app/config';
|
import { useAppText } from '@/i18n/state/client';
|
||||||
|
|
||||||
export default function AdminPhotosClient({
|
export default function AdminPhotosClient({
|
||||||
photos,
|
photos,
|
||||||
@ -42,6 +42,8 @@ export default function AdminPhotosClient({
|
|||||||
}) {
|
}) {
|
||||||
const { uploadState: { isUploading } } = useAppState();
|
const { uploadState: { isUploading } } = useAppState();
|
||||||
|
|
||||||
|
const appText = useAppText();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppGrid
|
<AppGrid
|
||||||
contentMain={
|
contentMain={
|
||||||
@ -64,8 +66,8 @@ export default function AdminPhotosClient({
|
|||||||
tooltip={(
|
tooltip={(
|
||||||
pluralize(
|
pluralize(
|
||||||
photosCountNeedsSync,
|
photosCountNeedsSync,
|
||||||
APP_TEXT.photo.photo,
|
appText.photo.photo,
|
||||||
APP_TEXT.photo.photoPlural,
|
appText.photo.photoPlural,
|
||||||
) +
|
) +
|
||||||
' missing data or AI-generated text'
|
' missing data or AI-generated text'
|
||||||
)}
|
)}
|
||||||
@ -83,8 +85,8 @@ export default function AdminPhotosClient({
|
|||||||
<ResponsiveText shortText={photosCountNeedsSync}>
|
<ResponsiveText shortText={photosCountNeedsSync}>
|
||||||
{pluralize(
|
{pluralize(
|
||||||
photosCountNeedsSync,
|
photosCountNeedsSync,
|
||||||
APP_TEXT.admin.update,
|
appText.admin.update,
|
||||||
APP_TEXT.admin.updatePlural,
|
appText.admin.updatePlural,
|
||||||
)}
|
)}
|
||||||
</ResponsiveText>
|
</ResponsiveText>
|
||||||
</PathLoaderButton>}
|
</PathLoaderButton>}
|
||||||
|
|||||||
@ -2,8 +2,9 @@ import { photoLabelForCount } from '@/photo';
|
|||||||
import { clsx } from 'clsx/lite';
|
import { clsx } from 'clsx/lite';
|
||||||
import Badge from '@/components/Badge';
|
import Badge from '@/components/Badge';
|
||||||
import PhotoRecipe from '@/recipe/PhotoRecipe';
|
import PhotoRecipe from '@/recipe/PhotoRecipe';
|
||||||
|
import { getAppText } from '@/i18n/state/server';
|
||||||
|
|
||||||
export default function AdminRecipeBadge({
|
export default async function AdminRecipeBadge({
|
||||||
recipe,
|
recipe,
|
||||||
count,
|
count,
|
||||||
hideBadge,
|
hideBadge,
|
||||||
@ -12,6 +13,8 @@ export default function AdminRecipeBadge({
|
|||||||
count: number,
|
count: number,
|
||||||
hideBadge?: boolean,
|
hideBadge?: boolean,
|
||||||
}) {
|
}) {
|
||||||
|
const appText = await getAppText();
|
||||||
|
|
||||||
const renderBadgeContent = () =>
|
const renderBadgeContent = () =>
|
||||||
<div className={clsx(
|
<div className={clsx(
|
||||||
'inline-flex items-center gap-2',
|
'inline-flex items-center gap-2',
|
||||||
@ -21,7 +24,7 @@ export default function AdminRecipeBadge({
|
|||||||
<span>{count}</span>
|
<span>{count}</span>
|
||||||
<span className="hidden xs:inline-block">
|
<span className="hidden xs:inline-block">
|
||||||
|
|
||||||
{photoLabelForCount(count)}
|
{photoLabelForCount(count, appText)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
|
|||||||
@ -9,12 +9,14 @@ import { pathForAdminRecipeEdit } from '@/app/paths';
|
|||||||
import { clsx } from 'clsx/lite';
|
import { clsx } from 'clsx/lite';
|
||||||
import { formatRecipe, Recipes, sortRecipes } from '@/recipe';
|
import { formatRecipe, Recipes, sortRecipes } from '@/recipe';
|
||||||
import AdminRecipeBadge from './AdminRecipeBadge';
|
import AdminRecipeBadge from './AdminRecipeBadge';
|
||||||
|
import { getAppText } from '@/i18n/state/server';
|
||||||
|
|
||||||
export default function AdminRecipeTable({
|
export default async function AdminRecipeTable({
|
||||||
recipes,
|
recipes,
|
||||||
}: {
|
}: {
|
||||||
recipes: Recipes
|
recipes: Recipes
|
||||||
}) {
|
}) {
|
||||||
|
const appText = await getAppText();
|
||||||
return (
|
return (
|
||||||
<AdminTable>
|
<AdminTable>
|
||||||
{sortRecipes(recipes).map(({ recipe, count }) =>
|
{sortRecipes(recipes).map(({ recipe, count }) =>
|
||||||
@ -31,7 +33,7 @@ export default function AdminRecipeTable({
|
|||||||
action={deletePhotoRecipeGloballyAction}
|
action={deletePhotoRecipeGloballyAction}
|
||||||
confirmText={
|
confirmText={
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
`Are you sure you want to remove "${formatRecipe(recipe)}" from ${photoQuantityText(count, false).toLowerCase()}?`}
|
`Are you sure you want to remove "${formatRecipe(recipe)}" from ${photoQuantityText(count, appText, false, false).toLowerCase()}?`}
|
||||||
>
|
>
|
||||||
<input type="hidden" name="recipe" value={recipe} />
|
<input type="hidden" name="recipe" value={recipe} />
|
||||||
<DeleteFormButton clearLocalState />
|
<DeleteFormButton clearLocalState />
|
||||||
|
|||||||
@ -4,8 +4,9 @@ import { clsx } from 'clsx/lite';
|
|||||||
import FavsTag from '@/tag/FavsTag';
|
import FavsTag from '@/tag/FavsTag';
|
||||||
import { isTagFavs } from '@/tag';
|
import { isTagFavs } from '@/tag';
|
||||||
import Badge from '@/components/Badge';
|
import Badge from '@/components/Badge';
|
||||||
|
import { getAppText } from '@/i18n/state/server';
|
||||||
|
|
||||||
export default function AdminTagBadge({
|
export default async function AdminTagBadge({
|
||||||
tag,
|
tag,
|
||||||
count,
|
count,
|
||||||
hideBadge,
|
hideBadge,
|
||||||
@ -14,6 +15,8 @@ export default function AdminTagBadge({
|
|||||||
count: number,
|
count: number,
|
||||||
hideBadge?: boolean,
|
hideBadge?: boolean,
|
||||||
}) {
|
}) {
|
||||||
|
const appText = await getAppText();
|
||||||
|
|
||||||
const renderBadgeContent = () =>
|
const renderBadgeContent = () =>
|
||||||
<div className={clsx(
|
<div className={clsx(
|
||||||
'inline-flex items-center gap-2',
|
'inline-flex items-center gap-2',
|
||||||
@ -28,7 +31,7 @@ export default function AdminTagBadge({
|
|||||||
<span>{count}</span>
|
<span>{count}</span>
|
||||||
<span className="hidden xs:inline-block">
|
<span className="hidden xs:inline-block">
|
||||||
|
|
||||||
{photoLabelForCount(count)}
|
{photoLabelForCount(count, appText)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
|
|||||||
@ -9,12 +9,15 @@ import EditButton from '@/admin/EditButton';
|
|||||||
import { pathForAdminTagEdit } from '@/app/paths';
|
import { pathForAdminTagEdit } from '@/app/paths';
|
||||||
import { clsx } from 'clsx/lite';
|
import { clsx } from 'clsx/lite';
|
||||||
import AdminTagBadge from './AdminTagBadge';
|
import AdminTagBadge from './AdminTagBadge';
|
||||||
|
import { getAppText } from '@/i18n/state/server';
|
||||||
|
|
||||||
export default function AdminTagTable({
|
export default async function AdminTagTable({
|
||||||
tags,
|
tags,
|
||||||
}: {
|
}: {
|
||||||
tags: Tags
|
tags: Tags
|
||||||
}) {
|
}) {
|
||||||
|
const appText = await getAppText();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminTable>
|
<AdminTable>
|
||||||
{sortTags(tags).map(({ tag, count }) =>
|
{sortTags(tags).map(({ tag, count }) =>
|
||||||
@ -31,7 +34,7 @@ export default function AdminTagTable({
|
|||||||
action={deletePhotoTagGloballyAction}
|
action={deletePhotoTagGloballyAction}
|
||||||
confirmText={
|
confirmText={
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
`Are you sure you want to remove "${formatTag(tag)}" from ${photoQuantityText(count, false).toLowerCase()}?`}
|
`Are you sure you want to remove "${formatTag(tag)}" from ${photoQuantityText(count, appText, false, false).toLowerCase()}?`}
|
||||||
>
|
>
|
||||||
<input type="hidden" name="tag" value={tag} />
|
<input type="hidden" name="tag" value={tag} />
|
||||||
<DeleteFormButton clearLocalState />
|
<DeleteFormButton clearLocalState />
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
import { deleteConfirmationTextForPhoto, Photo, titleForPhoto } from '@/photo';
|
import { deleteConfirmationTextForPhoto, Photo, titleForPhoto } from '@/photo';
|
||||||
import DeletePhotosButton from './DeletePhotosButton';
|
import DeletePhotosButton from './DeletePhotosButton';
|
||||||
import { ComponentProps } from 'react';
|
import { ComponentProps } from 'react';
|
||||||
|
import { useAppText } from '@/i18n/state/client';
|
||||||
|
|
||||||
export default function DeletePhotoButton({
|
export default function DeletePhotoButton({
|
||||||
photo,
|
photo,
|
||||||
@ -10,11 +11,12 @@ export default function DeletePhotoButton({
|
|||||||
}: {
|
}: {
|
||||||
photo: Photo
|
photo: Photo
|
||||||
} & ComponentProps<typeof DeletePhotosButton>) {
|
} & ComponentProps<typeof DeletePhotosButton>) {
|
||||||
|
const appText = useAppText();
|
||||||
return (
|
return (
|
||||||
<DeletePhotosButton
|
<DeletePhotosButton
|
||||||
{...rest}
|
{...rest}
|
||||||
photoIds={[photo.id]}
|
photoIds={[photo.id]}
|
||||||
confirmText={deleteConfirmationTextForPhoto(photo)}
|
confirmText={deleteConfirmationTextForPhoto(photo, appText)}
|
||||||
toastText={`"${titleForPhoto(photo)}" deleted`}
|
toastText={`"${titleForPhoto(photo)}" deleted`}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { useAppState } from '@/state/AppState';
|
|||||||
import { toastSuccess, toastWarning } from '@/toast';
|
import { toastSuccess, toastWarning } from '@/toast';
|
||||||
import { ComponentProps, useState } from 'react';
|
import { ComponentProps, useState } from 'react';
|
||||||
import DeleteButton from './DeleteButton';
|
import DeleteButton from './DeleteButton';
|
||||||
|
import { useAppText } from '@/i18n/state/client';
|
||||||
|
|
||||||
export default function DeletePhotosButton({
|
export default function DeletePhotosButton({
|
||||||
photoIds = [],
|
photoIds = [],
|
||||||
@ -27,7 +28,9 @@ export default function DeletePhotosButton({
|
|||||||
} & ComponentProps<typeof LoaderButton>) {
|
} & ComponentProps<typeof LoaderButton>) {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const photosText = photoQuantityText(photoIds.length, false, false);
|
const appText = useAppText();
|
||||||
|
|
||||||
|
const photosText = photoQuantityText(photoIds.length, appText, false, false);
|
||||||
|
|
||||||
const { invalidateSwr, registerAdminUpdate } = useAppState();
|
const { invalidateSwr, registerAdminUpdate } = useAppState();
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import FieldSetWithStatus from '@/components/FieldSetWithStatus';
|
import FieldSetWithStatus from '@/components/FieldSetWithStatus';
|
||||||
|
import { useAppText } from '@/i18n/state/client';
|
||||||
import { convertTagsForForm, getValidationMessageForTags, Tags } from '@/tag';
|
import { convertTagsForForm, getValidationMessageForTags, Tags } from '@/tag';
|
||||||
import { ComponentProps, useEffect, useRef, useState } from 'react';
|
import { ComponentProps, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
@ -25,6 +26,8 @@ export default function PhotoTagFieldset(props: {
|
|||||||
|
|
||||||
const ref = useRef<HTMLInputElement>(null);
|
const ref = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const appText = useAppText();
|
||||||
|
|
||||||
const [errorMessageLocal, setErrorMessageLocal] = useState('');
|
const [errorMessageLocal, setErrorMessageLocal] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -43,7 +46,7 @@ export default function PhotoTagFieldset(props: {
|
|||||||
inputRef={ref}
|
inputRef={ref}
|
||||||
label="Tags"
|
label="Tags"
|
||||||
value={tags}
|
value={tags}
|
||||||
tagOptions={convertTagsForForm(tagOptions)}
|
tagOptions={convertTagsForForm(tagOptions, appText)}
|
||||||
onChange={tags => {
|
onChange={tags => {
|
||||||
onChange(tags);
|
onChange(tags);
|
||||||
const validationMessage = getValidationMessageForTags(tags) ?? '';
|
const validationMessage = getValidationMessageForTags(tags) ?? '';
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { useAppState } from '@/state/AppState';
|
|||||||
import SignInForm from '@/auth/SignInForm';
|
import SignInForm from '@/auth/SignInForm';
|
||||||
import clsx from 'clsx/lite';
|
import clsx from 'clsx/lite';
|
||||||
import PhotoUploadWithStatus from '@/photo/PhotoUploadWithStatus';
|
import PhotoUploadWithStatus from '@/photo/PhotoUploadWithStatus';
|
||||||
import { APP_TEXT } from '@/app/config';
|
import { useAppText } from '@/i18n/state/client';
|
||||||
|
|
||||||
export default function SignInOrUploadClient({
|
export default function SignInOrUploadClient({
|
||||||
shouldResize,
|
shouldResize,
|
||||||
@ -15,6 +15,8 @@ export default function SignInOrUploadClient({
|
|||||||
}) {
|
}) {
|
||||||
const { isUserSignedIn, isCheckingAuth } = useAppState();
|
const { isUserSignedIn, isCheckingAuth } = useAppState();
|
||||||
|
|
||||||
|
const appText = useAppText();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx(
|
<div className={clsx(
|
||||||
'flex justify-center items-center flex-col gap-4',
|
'flex justify-center items-center flex-col gap-4',
|
||||||
@ -22,10 +24,10 @@ export default function SignInOrUploadClient({
|
|||||||
)}>
|
)}>
|
||||||
<div>
|
<div>
|
||||||
{isCheckingAuth
|
{isCheckingAuth
|
||||||
? APP_TEXT.misc.loading
|
? appText.misc.loading
|
||||||
: isUserSignedIn
|
: isUserSignedIn
|
||||||
? APP_TEXT.onboarding.setupFirstPhoto
|
? appText.onboarding.setupFirstPhoto
|
||||||
: APP_TEXT.onboarding.setupSignIn}
|
: appText.onboarding.setupSignIn}
|
||||||
</div>
|
</div>
|
||||||
{!isCheckingAuth && isUserSignedIn === false &&
|
{!isCheckingAuth && isUserSignedIn === false &&
|
||||||
<div className="flex justify-center my-2 sm:my-4">
|
<div className="flex justify-center my-2 sm:my-4">
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import {
|
|||||||
import IconSearch from '../components/icons/IconSearch';
|
import IconSearch from '../components/icons/IconSearch';
|
||||||
import { useAppState } from '@/state/AppState';
|
import { useAppState } from '@/state/AppState';
|
||||||
import {
|
import {
|
||||||
APP_TEXT,
|
|
||||||
GRID_HOMEPAGE_ENABLED,
|
GRID_HOMEPAGE_ENABLED,
|
||||||
SHOW_KEYBOARD_SHORTCUT_TOOLTIPS,
|
SHOW_KEYBOARD_SHORTCUT_TOOLTIPS,
|
||||||
} from './config';
|
} from './config';
|
||||||
@ -20,6 +19,7 @@ import { useCallback, useRef, useState } from 'react';
|
|||||||
import useKeydownHandler from '@/utility/useKeydownHandler';
|
import useKeydownHandler from '@/utility/useKeydownHandler';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import { KEY_COMMANDS } from '@/photo/key-commands';
|
import { KEY_COMMANDS } from '@/photo/key-commands';
|
||||||
|
import { useAppText } from '@/i18n/state/client';
|
||||||
|
|
||||||
export type SwitcherSelection = 'feed' | 'grid' | 'admin';
|
export type SwitcherSelection = 'feed' | 'grid' | 'admin';
|
||||||
|
|
||||||
@ -32,6 +32,8 @@ export default function AppViewSwitcher({
|
|||||||
}) {
|
}) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
const appText = useAppText();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isUserSignedIn,
|
isUserSignedIn,
|
||||||
isUserSignedInEager,
|
isUserSignedInEager,
|
||||||
@ -67,7 +69,7 @@ export default function AppViewSwitcher({
|
|||||||
hrefRef={refHrefFeed}
|
hrefRef={refHrefFeed}
|
||||||
active={currentSelection === 'feed'}
|
active={currentSelection === 'feed'}
|
||||||
tooltip={{...SHOW_KEYBOARD_SHORTCUT_TOOLTIPS && {
|
tooltip={{...SHOW_KEYBOARD_SHORTCUT_TOOLTIPS && {
|
||||||
content: APP_TEXT.nav.feed,
|
content: appText.nav.feed,
|
||||||
keyCommand: KEY_COMMANDS.feed,
|
keyCommand: KEY_COMMANDS.feed,
|
||||||
}}}
|
}}}
|
||||||
noPadding
|
noPadding
|
||||||
@ -80,7 +82,7 @@ export default function AppViewSwitcher({
|
|||||||
hrefRef={refHrefGrid}
|
hrefRef={refHrefGrid}
|
||||||
active={currentSelection === 'grid'}
|
active={currentSelection === 'grid'}
|
||||||
tooltip={{...SHOW_KEYBOARD_SHORTCUT_TOOLTIPS && {
|
tooltip={{...SHOW_KEYBOARD_SHORTCUT_TOOLTIPS && {
|
||||||
content: APP_TEXT.nav.grid,
|
content: appText.nav.grid,
|
||||||
keyCommand: KEY_COMMANDS.grid,
|
keyCommand: KEY_COMMANDS.grid,
|
||||||
}}}
|
}}}
|
||||||
noPadding
|
noPadding
|
||||||
@ -104,7 +106,7 @@ export default function AppViewSwitcher({
|
|||||||
noPadding
|
noPadding
|
||||||
tooltip={{
|
tooltip={{
|
||||||
...!isAdminMenuOpen && SHOW_KEYBOARD_SHORTCUT_TOOLTIPS && {
|
...!isAdminMenuOpen && SHOW_KEYBOARD_SHORTCUT_TOOLTIPS && {
|
||||||
content: APP_TEXT.nav.admin,
|
content: appText.nav.admin,
|
||||||
keyCommand: KEY_COMMANDS.admin,
|
keyCommand: KEY_COMMANDS.admin,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
@ -117,7 +119,7 @@ export default function AppViewSwitcher({
|
|||||||
/>}
|
/>}
|
||||||
tooltip={{
|
tooltip={{
|
||||||
...!isAdminMenuOpen && SHOW_KEYBOARD_SHORTCUT_TOOLTIPS && {
|
...!isAdminMenuOpen && SHOW_KEYBOARD_SHORTCUT_TOOLTIPS && {
|
||||||
content: APP_TEXT.nav.admin,
|
content: appText.nav.admin,
|
||||||
keyCommand: KEY_COMMANDS.admin,
|
keyCommand: KEY_COMMANDS.admin,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
@ -129,7 +131,7 @@ export default function AppViewSwitcher({
|
|||||||
icon={<IconSearch includeTitle={false} />}
|
icon={<IconSearch includeTitle={false} />}
|
||||||
onClick={() => setIsCommandKOpen?.(true)}
|
onClick={() => setIsCommandKOpen?.(true)}
|
||||||
tooltip={{...SHOW_KEYBOARD_SHORTCUT_TOOLTIPS && {
|
tooltip={{...SHOW_KEYBOARD_SHORTCUT_TOOLTIPS && {
|
||||||
content: APP_TEXT.nav.search,
|
content: appText.nav.search,
|
||||||
keyCommandModifier: KEY_COMMANDS.search[0],
|
keyCommandModifier: KEY_COMMANDS.search[0],
|
||||||
keyCommand: KEY_COMMANDS.search[1],
|
keyCommand: KEY_COMMANDS.search[1],
|
||||||
}}}
|
}}}
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { clsx } from 'clsx/lite';
|
|||||||
import AppGrid from '../components/AppGrid';
|
import AppGrid from '../components/AppGrid';
|
||||||
import ThemeSwitcher from '@/app/ThemeSwitcher';
|
import ThemeSwitcher from '@/app/ThemeSwitcher';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { APP_TEXT, SHOW_REPO_LINK } from '@/app/config';
|
import { SHOW_REPO_LINK } from '@/app/config';
|
||||||
import RepoLink from '../components/RepoLink';
|
import RepoLink from '../components/RepoLink';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import { PATH_ADMIN_PHOTOS, isPathAdmin, isPathSignIn } from './paths';
|
import { PATH_ADMIN_PHOTOS, isPathAdmin, isPathSignIn } from './paths';
|
||||||
@ -13,6 +13,7 @@ import { signOutAction } from '@/auth/actions';
|
|||||||
import AnimateItems from '@/components/AnimateItems';
|
import AnimateItems from '@/components/AnimateItems';
|
||||||
import { useAppState } from '@/state/AppState';
|
import { useAppState } from '@/state/AppState';
|
||||||
import Spinner from '@/components/Spinner';
|
import Spinner from '@/components/Spinner';
|
||||||
|
import { useAppText } from '@/i18n/state/client';
|
||||||
|
|
||||||
export default function Footer() {
|
export default function Footer() {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
@ -24,6 +25,8 @@ export default function Footer() {
|
|||||||
clearAuthStateAndRedirectIfNecessary,
|
clearAuthStateAndRedirectIfNecessary,
|
||||||
} = useAppState();
|
} = useAppState();
|
||||||
|
|
||||||
|
const appText = useAppText();
|
||||||
|
|
||||||
const showFooter = !isPathSignIn(pathname);
|
const showFooter = !isPathSignIn(pathname);
|
||||||
|
|
||||||
const shouldAnimate = !isPathAdmin(pathname);
|
const shouldAnimate = !isPathAdmin(pathname);
|
||||||
@ -51,7 +54,7 @@ export default function Footer() {
|
|||||||
<form action={() => signOutAction()
|
<form action={() => signOutAction()
|
||||||
.then(clearAuthStateAndRedirectIfNecessary)}>
|
.then(clearAuthStateAndRedirectIfNecessary)}>
|
||||||
<SubmitButtonWithStatus styleAs="link">
|
<SubmitButtonWithStatus styleAs="link">
|
||||||
{APP_TEXT.auth.signOut}
|
{appText.auth.signOut}
|
||||||
</SubmitButtonWithStatus>
|
</SubmitButtonWithStatus>
|
||||||
</form>
|
</form>
|
||||||
</>
|
</>
|
||||||
@ -60,7 +63,7 @@ export default function Footer() {
|
|||||||
: SHOW_REPO_LINK
|
: SHOW_REPO_LINK
|
||||||
? <RepoLink />
|
? <RepoLink />
|
||||||
: <Link href={PATH_ADMIN_PHOTOS}>
|
: <Link href={PATH_ADMIN_PHOTOS}>
|
||||||
{APP_TEXT.nav.admin}
|
{appText.nav.admin}
|
||||||
</Link>}
|
</Link>}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center h-10">
|
<div className="flex items-center h-10">
|
||||||
|
|||||||
@ -5,9 +5,11 @@ import { useTheme } from 'next-themes';
|
|||||||
import Switcher from '@/components/Switcher';
|
import Switcher from '@/components/Switcher';
|
||||||
import SwitcherItem from '@/components/SwitcherItem';
|
import SwitcherItem from '@/components/SwitcherItem';
|
||||||
import { BiDesktop, BiMoon, BiSun } from 'react-icons/bi';
|
import { BiDesktop, BiMoon, BiSun } from 'react-icons/bi';
|
||||||
import { APP_TEXT } from './config';
|
import { useAppText } from '@/i18n/state/client';
|
||||||
|
|
||||||
export default function ThemeSwitcher () {
|
export default function ThemeSwitcher () {
|
||||||
|
const appText = useAppText();
|
||||||
|
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
const { theme, setTheme } = useTheme();
|
const { theme, setTheme } = useTheme();
|
||||||
|
|
||||||
@ -26,19 +28,19 @@ export default function ThemeSwitcher () {
|
|||||||
icon={<BiDesktop size={16} />}
|
icon={<BiDesktop size={16} />}
|
||||||
onClick={() => setTheme('system')}
|
onClick={() => setTheme('system')}
|
||||||
active={theme === 'system'}
|
active={theme === 'system'}
|
||||||
tooltip={{ content: APP_TEXT.theme.system }}
|
tooltip={{ content: appText.theme.system }}
|
||||||
/>
|
/>
|
||||||
<SwitcherItem
|
<SwitcherItem
|
||||||
icon={<BiSun size={18} />}
|
icon={<BiSun size={18} />}
|
||||||
onClick={() => setTheme('light')}
|
onClick={() => setTheme('light')}
|
||||||
active={theme === 'light'}
|
active={theme === 'light'}
|
||||||
tooltip={{ content: APP_TEXT.theme.light }}
|
tooltip={{ content: appText.theme.light }}
|
||||||
/>
|
/>
|
||||||
<SwitcherItem
|
<SwitcherItem
|
||||||
icon={<BiMoon size={16} />}
|
icon={<BiMoon size={16} />}
|
||||||
onClick={() => setTheme('dark')}
|
onClick={() => setTheme('dark')}
|
||||||
active={theme === 'dark'}
|
active={theme === 'dark'}
|
||||||
tooltip={{ content: APP_TEXT.theme.dark }}
|
tooltip={{ content: appText.theme.dark }}
|
||||||
/>
|
/>
|
||||||
</Switcher>
|
</Switcher>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import {
|
|||||||
import { getOrderedCategoriesFromString } from '@/category';
|
import { getOrderedCategoriesFromString } from '@/category';
|
||||||
import type { StorageType } from '@/platforms/storage';
|
import type { StorageType } from '@/platforms/storage';
|
||||||
import { makeUrlAbsolute, shortenUrl } from '@/utility/url';
|
import { makeUrlAbsolute, shortenUrl } from '@/utility/url';
|
||||||
import { getTextForLocale } from '@/i18n';
|
|
||||||
|
|
||||||
// HARD-CODED GLOBAL CONFIGURATION
|
// HARD-CODED GLOBAL CONFIGURATION
|
||||||
|
|
||||||
@ -99,9 +98,7 @@ const SITE_DOMAIN_SHORT = shortenUrl(SITE_DOMAIN);
|
|||||||
|
|
||||||
// SITE META
|
// SITE META
|
||||||
|
|
||||||
export const APP_TEXT = getTextForLocale(
|
export const APP_LOCALE = process.env.NEXT_PUBLIC_LOCALE || 'US-EN';
|
||||||
process.env.NEXT_PUBLIC_LOCALE,
|
|
||||||
);
|
|
||||||
|
|
||||||
export const NAV_TITLE =
|
export const NAV_TITLE =
|
||||||
process.env.NEXT_PUBLIC_NAV_TITLE;
|
process.env.NEXT_PUBLIC_NAV_TITLE;
|
||||||
@ -342,15 +339,14 @@ export const APP_CONFIGURATION = {
|
|||||||
Boolean(process.env.ADMIN_EMAIL) &&
|
Boolean(process.env.ADMIN_EMAIL) &&
|
||||||
Boolean(process.env.ADMIN_PASSWORD)
|
Boolean(process.env.ADMIN_PASSWORD)
|
||||||
),
|
),
|
||||||
// Domain
|
// Content
|
||||||
locale: process.env.NEXT_PUBLIC_LOCALE ?? 'US-EN',
|
locale: APP_LOCALE,
|
||||||
hasLocale: Boolean(process.env.NEXT_PUBLIC_LOCALE),
|
hasLocale: Boolean(process.env.NEXT_PUBLIC_LOCALE),
|
||||||
hasDomain: Boolean(
|
hasDomain: Boolean(
|
||||||
process.env.NEXT_PUBLIC_DOMAIN ||
|
process.env.NEXT_PUBLIC_DOMAIN ||
|
||||||
// Legacy environment variable
|
// Legacy environment variable
|
||||||
process.env.NEXT_PUBLIC_SITE_DOMAIN,
|
process.env.NEXT_PUBLIC_SITE_DOMAIN,
|
||||||
),
|
),
|
||||||
// Content
|
|
||||||
hasNavTitle: Boolean(NAV_TITLE),
|
hasNavTitle: Boolean(NAV_TITLE),
|
||||||
hasNavCaption: Boolean(NAV_CAPTION),
|
hasNavCaption: Boolean(NAV_CAPTION),
|
||||||
isMetaTitleConfigured: IS_META_TITLE_CONFIGURED,
|
isMetaTitleConfigured: IS_META_TITLE_CONFIGURED,
|
||||||
|
|||||||
@ -21,7 +21,7 @@ import { useAppState } from '@/state/AppState';
|
|||||||
import { clsx } from 'clsx/lite';
|
import { clsx } from 'clsx/lite';
|
||||||
import { PATH_ADMIN_PHOTOS } from '@/app/paths';
|
import { PATH_ADMIN_PHOTOS } from '@/app/paths';
|
||||||
import IconLock from '@/components/icons/IconLock';
|
import IconLock from '@/components/icons/IconLock';
|
||||||
import { APP_TEXT } from '@/app/config';
|
import { useAppText } from '@/i18n/state/client';
|
||||||
|
|
||||||
export default function SignInForm({
|
export default function SignInForm({
|
||||||
includeTitle = true,
|
includeTitle = true,
|
||||||
@ -36,6 +36,8 @@ export default function SignInForm({
|
|||||||
|
|
||||||
const { setUserEmail } = useAppState();
|
const { setUserEmail } = useAppState();
|
||||||
|
|
||||||
|
const appText = useAppText();
|
||||||
|
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [response, action] = useActionState(signInAction, undefined);
|
const [response, action] = useActionState(signInAction, undefined);
|
||||||
@ -80,27 +82,27 @@ export default function SignInForm({
|
|||||||
)}>
|
)}>
|
||||||
<IconLock className="text-main translate-y-[0.5px]" />
|
<IconLock className="text-main translate-y-[0.5px]" />
|
||||||
<span className="text-main">
|
<span className="text-main">
|
||||||
{APP_TEXT.auth.signIn}
|
{appText.auth.signIn}
|
||||||
</span>
|
</span>
|
||||||
</h1>}
|
</h1>}
|
||||||
<form action={action} className="w-full">
|
<form action={action} className="w-full">
|
||||||
<div className="space-y-5 w-full -translate-y-0.5">
|
<div className="space-y-5 w-full -translate-y-0.5">
|
||||||
{response === KEY_CREDENTIALS_SIGN_IN_ERROR &&
|
{response === KEY_CREDENTIALS_SIGN_IN_ERROR &&
|
||||||
<ErrorNote>
|
<ErrorNote>
|
||||||
{APP_TEXT.auth.invalidEmailPassword}
|
{appText.auth.invalidEmailPassword}
|
||||||
</ErrorNote>}
|
</ErrorNote>}
|
||||||
<div className="space-y-4 w-full">
|
<div className="space-y-4 w-full">
|
||||||
<FieldSetWithStatus
|
<FieldSetWithStatus
|
||||||
id="email"
|
id="email"
|
||||||
inputRef={emailRef}
|
inputRef={emailRef}
|
||||||
label={APP_TEXT.auth.email}
|
label={appText.auth.email}
|
||||||
type="email"
|
type="email"
|
||||||
value={email}
|
value={email}
|
||||||
onChange={setEmail}
|
onChange={setEmail}
|
||||||
/>
|
/>
|
||||||
<FieldSetWithStatus
|
<FieldSetWithStatus
|
||||||
id="password"
|
id="password"
|
||||||
label={APP_TEXT.auth.password}
|
label={appText.auth.password}
|
||||||
type="password"
|
type="password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={setPassword}
|
onChange={setPassword}
|
||||||
@ -113,7 +115,7 @@ export default function SignInForm({
|
|||||||
/>}
|
/>}
|
||||||
</div>
|
</div>
|
||||||
<SubmitButtonWithStatus disabled={!isFormValid}>
|
<SubmitButtonWithStatus disabled={!isFormValid}>
|
||||||
{APP_TEXT.auth.signIn}
|
{appText.auth.signIn}
|
||||||
</SubmitButtonWithStatus>
|
</SubmitButtonWithStatus>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -4,8 +4,9 @@ import { Camera, cameraFromPhoto } from '.';
|
|||||||
import PhotoCamera from './PhotoCamera';
|
import PhotoCamera from './PhotoCamera';
|
||||||
import { descriptionForCameraPhotos } from './meta';
|
import { descriptionForCameraPhotos } from './meta';
|
||||||
import { AI_TEXT_GENERATION_ENABLED } from '@/app/config';
|
import { AI_TEXT_GENERATION_ENABLED } from '@/app/config';
|
||||||
|
import { getAppText } from '@/i18n/state/server';
|
||||||
|
|
||||||
export default function CameraHeader({
|
export default async function CameraHeader({
|
||||||
camera: cameraProp,
|
camera: cameraProp,
|
||||||
photos,
|
photos,
|
||||||
selectedPhoto,
|
selectedPhoto,
|
||||||
@ -21,12 +22,19 @@ export default function CameraHeader({
|
|||||||
dateRange?: PhotoDateRange
|
dateRange?: PhotoDateRange
|
||||||
}) {
|
}) {
|
||||||
const camera = cameraFromPhoto(photos[0], cameraProp);
|
const camera = cameraFromPhoto(photos[0], cameraProp);
|
||||||
|
const appText = await getAppText();
|
||||||
return (
|
return (
|
||||||
<PhotoHeader
|
<PhotoHeader
|
||||||
camera={camera}
|
camera={camera}
|
||||||
entity={<PhotoCamera {...{ camera }} contrast="high" />}
|
entity={<PhotoCamera {...{ camera }} contrast="high" />}
|
||||||
entityDescription={
|
entityDescription={
|
||||||
descriptionForCameraPhotos(photos, undefined, count, dateRange)}
|
descriptionForCameraPhotos(
|
||||||
|
photos,
|
||||||
|
appText,
|
||||||
|
undefined,
|
||||||
|
count,
|
||||||
|
dateRange,
|
||||||
|
)}
|
||||||
photos={photos}
|
photos={photos}
|
||||||
selectedPhoto={selectedPhoto}
|
selectedPhoto={selectedPhoto}
|
||||||
indexNumber={indexNumber}
|
indexNumber={indexNumber}
|
||||||
|
|||||||
@ -3,8 +3,9 @@ import { absolutePathForCameraImage, pathForCamera } from '@/app/paths';
|
|||||||
import OGTile, { OGLoadingState } from '@/components/OGTile';
|
import OGTile, { OGLoadingState } from '@/components/OGTile';
|
||||||
import { Camera } from '.';
|
import { Camera } from '.';
|
||||||
import { descriptionForCameraPhotos, titleForCamera } from './meta';
|
import { descriptionForCameraPhotos, titleForCamera } from './meta';
|
||||||
|
import { getAppText } from '@/i18n/state/server';
|
||||||
|
|
||||||
export default function CameraOGTile({
|
export default async function CameraOGTile({
|
||||||
camera,
|
camera,
|
||||||
photos,
|
photos,
|
||||||
loadingState: loadingStateExternal,
|
loadingState: loadingStateExternal,
|
||||||
@ -25,10 +26,17 @@ export default function CameraOGTile({
|
|||||||
count?: number
|
count?: number
|
||||||
dateRange?: PhotoDateRange
|
dateRange?: PhotoDateRange
|
||||||
}) {
|
}) {
|
||||||
|
const appText = await getAppText();
|
||||||
return (
|
return (
|
||||||
<OGTile {...{
|
<OGTile {...{
|
||||||
title: titleForCamera(camera, photos, count),
|
title: titleForCamera(camera, photos, appText, count),
|
||||||
description: descriptionForCameraPhotos(photos, true, count, dateRange),
|
description: descriptionForCameraPhotos(
|
||||||
|
photos,
|
||||||
|
appText,
|
||||||
|
true,
|
||||||
|
count,
|
||||||
|
dateRange,
|
||||||
|
),
|
||||||
path: pathForCamera(camera),
|
path: pathForCamera(camera),
|
||||||
pathImageAbsolute: absolutePathForCameraImage(camera),
|
pathImageAbsolute: absolutePathForCameraImage(camera),
|
||||||
loadingState: loadingStateExternal,
|
loadingState: loadingStateExternal,
|
||||||
|
|||||||
@ -4,8 +4,9 @@ import ShareModal from '@/share/ShareModal';
|
|||||||
import CameraOGTile from './CameraOGTile';
|
import CameraOGTile from './CameraOGTile';
|
||||||
import { Camera, formatCameraText } from '.';
|
import { Camera, formatCameraText } from '.';
|
||||||
import { shareTextForCamera } from './meta';
|
import { shareTextForCamera } from './meta';
|
||||||
|
import { getAppText } from '@/i18n/state/server';
|
||||||
|
|
||||||
export default function CameraShareModal({
|
export default async function CameraShareModal({
|
||||||
camera,
|
camera,
|
||||||
photos,
|
photos,
|
||||||
count,
|
count,
|
||||||
@ -13,11 +14,12 @@ export default function CameraShareModal({
|
|||||||
}: {
|
}: {
|
||||||
camera: Camera
|
camera: Camera
|
||||||
} & PhotoSetAttributes) {
|
} & PhotoSetAttributes) {
|
||||||
|
const appText = await getAppText();
|
||||||
return (
|
return (
|
||||||
<ShareModal
|
<ShareModal
|
||||||
pathShare={absolutePathForCamera(camera, true)}
|
pathShare={absolutePathForCamera(camera, true)}
|
||||||
navigatorTitle={formatCameraText(camera)}
|
navigatorTitle={formatCameraText(camera)}
|
||||||
socialText={shareTextForCamera(camera, photos)}
|
socialText={shareTextForCamera(camera, photos, appText)}
|
||||||
>
|
>
|
||||||
<CameraOGTile {...{ camera, photos, count, dateRange }} />
|
<CameraOGTile {...{ camera, photos, count, dateRange }} />
|
||||||
</ShareModal>
|
</ShareModal>
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import {
|
|||||||
absolutePathForCamera,
|
absolutePathForCamera,
|
||||||
absolutePathForCameraImage,
|
absolutePathForCameraImage,
|
||||||
} from '@/app/paths';
|
} from '@/app/paths';
|
||||||
import { APP_TEXT } from '@/app/config';
|
import { I18NState } from '@/i18n/state';
|
||||||
|
|
||||||
// Meta functions moved to separate file to avoid
|
// Meta functions moved to separate file to avoid
|
||||||
// dependencies (camelcase-keys) found in photo/index.ts
|
// dependencies (camelcase-keys) found in photo/index.ts
|
||||||
@ -18,30 +18,34 @@ import { APP_TEXT } from '@/app/config';
|
|||||||
export const titleForCamera = (
|
export const titleForCamera = (
|
||||||
camera: Camera,
|
camera: Camera,
|
||||||
photos: Photo[],
|
photos: Photo[],
|
||||||
|
appText: I18NState,
|
||||||
explicitCount?: number,
|
explicitCount?: number,
|
||||||
) => [
|
) => [
|
||||||
APP_TEXT.category.cameraTitle(
|
appText.category.cameraTitle(
|
||||||
formatCameraText(cameraFromPhoto(photos[0], camera)),
|
formatCameraText(cameraFromPhoto(photos[0], camera)),
|
||||||
),
|
),
|
||||||
photoQuantityText(explicitCount ?? photos.length),
|
photoQuantityText(explicitCount ?? photos.length, appText),
|
||||||
].join(' ');
|
].join(' ');
|
||||||
|
|
||||||
export const shareTextForCamera = (
|
export const shareTextForCamera = (
|
||||||
camera: Camera,
|
camera: Camera,
|
||||||
photos: Photo[],
|
photos: Photo[],
|
||||||
|
appText: I18NState,
|
||||||
) =>
|
) =>
|
||||||
APP_TEXT.category.cameraShare(
|
appText.category.cameraShare(
|
||||||
formatCameraText(cameraFromPhoto(photos[0], camera)),
|
formatCameraText(cameraFromPhoto(photos[0], camera)),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const descriptionForCameraPhotos = (
|
export const descriptionForCameraPhotos = (
|
||||||
photos: Photo[],
|
photos: Photo[],
|
||||||
|
appText: I18NState,
|
||||||
dateBased?: boolean,
|
dateBased?: boolean,
|
||||||
explicitCount?: number,
|
explicitCount?: number,
|
||||||
explicitDateRange?: PhotoDateRange,
|
explicitDateRange?: PhotoDateRange,
|
||||||
) =>
|
) =>
|
||||||
descriptionForPhotoSet(
|
descriptionForPhotoSet(
|
||||||
photos,
|
photos,
|
||||||
|
appText,
|
||||||
undefined,
|
undefined,
|
||||||
dateBased,
|
dateBased,
|
||||||
explicitCount,
|
explicitCount,
|
||||||
@ -51,12 +55,19 @@ export const descriptionForCameraPhotos = (
|
|||||||
export const generateMetaForCamera = (
|
export const generateMetaForCamera = (
|
||||||
camera: Camera,
|
camera: Camera,
|
||||||
photos: Photo[],
|
photos: Photo[],
|
||||||
|
appText: I18NState,
|
||||||
explicitCount?: number,
|
explicitCount?: number,
|
||||||
explicitDateRange?: PhotoDateRange,
|
explicitDateRange?: PhotoDateRange,
|
||||||
) => ({
|
) => ({
|
||||||
url: absolutePathForCamera(camera),
|
url: absolutePathForCamera(camera),
|
||||||
title: titleForCamera(camera, photos, explicitCount),
|
title: titleForCamera(camera, photos, appText, explicitCount),
|
||||||
description:
|
description:
|
||||||
descriptionForCameraPhotos(photos, true, explicitCount, explicitDateRange),
|
descriptionForCameraPhotos(
|
||||||
|
photos,
|
||||||
|
appText,
|
||||||
|
true,
|
||||||
|
explicitCount,
|
||||||
|
explicitDateRange,
|
||||||
|
),
|
||||||
images: absolutePathForCameraImage(camera),
|
images: absolutePathForCameraImage(camera),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { getPhotosMetaCached } from '@/photo/cache';
|
|||||||
import { photoQuantityText } from '@/photo';
|
import { photoQuantityText } from '@/photo';
|
||||||
import { ADMIN_DEBUG_TOOLS_ENABLED } from '../app/config';
|
import { ADMIN_DEBUG_TOOLS_ENABLED } from '../app/config';
|
||||||
import { getDataForCategoriesCached } from '@/category/cache';
|
import { getDataForCategoriesCached } from '@/category/cache';
|
||||||
|
import { getAppText } from '@/i18n/state/server';
|
||||||
|
|
||||||
export default async function CommandK() {
|
export default async function CommandK() {
|
||||||
const [
|
const [
|
||||||
@ -15,9 +16,13 @@ export default async function CommandK() {
|
|||||||
getDataForCategoriesCached(),
|
getDataForCategoriesCached(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return <CommandKClient
|
const appText = await getAppText();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CommandKClient
|
||||||
{...categories}
|
{...categories}
|
||||||
showDebugTools={ADMIN_DEBUG_TOOLS_ENABLED}
|
showDebugTools={ADMIN_DEBUG_TOOLS_ENABLED}
|
||||||
footer={photoQuantityText(count, false)}
|
footer={photoQuantityText(count, appText, false)}
|
||||||
/>;
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -53,7 +53,6 @@ import { addHiddenToTags, formatTag, isTagFavs, isTagHidden } from '@/tag';
|
|||||||
import { formatCount, formatCountDescriptive } from '@/utility/string';
|
import { formatCount, formatCountDescriptive } from '@/utility/string';
|
||||||
import CommandKItem from './CommandKItem';
|
import CommandKItem from './CommandKItem';
|
||||||
import {
|
import {
|
||||||
APP_TEXT,
|
|
||||||
CATEGORY_VISIBILITY,
|
CATEGORY_VISIBILITY,
|
||||||
GRID_HOMEPAGE_ENABLED,
|
GRID_HOMEPAGE_ENABLED,
|
||||||
} from '@/app/config';
|
} from '@/app/config';
|
||||||
@ -78,6 +77,7 @@ 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 IconHidden from '@/components/icons/IconHidden';
|
||||||
|
import { useAppText } from '@/i18n/state/client';
|
||||||
|
|
||||||
const DIALOG_TITLE = 'Global Command-K Menu';
|
const DIALOG_TITLE = 'Global Command-K Menu';
|
||||||
const DIALOG_DESCRIPTION = 'For searching photos, views, and settings';
|
const DIALOG_DESCRIPTION = 'For searching photos, views, and settings';
|
||||||
@ -157,6 +157,8 @@ export default function CommandKClient({
|
|||||||
setShouldDebugRecipeOverlays,
|
setShouldDebugRecipeOverlays,
|
||||||
} = useAppState();
|
} = useAppState();
|
||||||
|
|
||||||
|
const appText = useAppText();
|
||||||
|
|
||||||
const isOpenRef = useRef(isOpen);
|
const isOpenRef = useRef(isOpen);
|
||||||
|
|
||||||
const refInput = useRef<HTMLInputElement>(null);
|
const refInput = useRef<HTMLInputElement>(null);
|
||||||
@ -261,7 +263,7 @@ export default function CommandKClient({
|
|||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [queryDebounced, isPending]);
|
}, [queryDebounced, isPending, appText]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (queryLive === '') {
|
if (queryLive === '') {
|
||||||
@ -289,7 +291,7 @@ export default function CommandKClient({
|
|||||||
.map(category => {
|
.map(category => {
|
||||||
switch (category) {
|
switch (category) {
|
||||||
case 'cameras': return {
|
case 'cameras': return {
|
||||||
heading: APP_TEXT.category.cameraPlural,
|
heading: appText.category.cameraPlural,
|
||||||
accessory: <IconCamera size={14} />,
|
accessory: <IconCamera size={14} />,
|
||||||
items: cameras.map(({ camera, count }) => ({
|
items: cameras.map(({ camera, count }) => ({
|
||||||
label: formatCameraText(camera),
|
label: formatCameraText(camera),
|
||||||
@ -299,7 +301,7 @@ export default function CommandKClient({
|
|||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
case 'lenses': return {
|
case 'lenses': return {
|
||||||
heading: APP_TEXT.category.lensPlural,
|
heading: appText.category.lensPlural,
|
||||||
accessory: <IconLens size={14} className="translate-y-[0.5px]" />,
|
accessory: <IconLens size={14} className="translate-y-[0.5px]" />,
|
||||||
items: lenses.map(({ lens, count }) => ({
|
items: lenses.map(({ lens, count }) => ({
|
||||||
label: formatLensText(lens, 'medium'),
|
label: formatLensText(lens, 'medium'),
|
||||||
@ -310,7 +312,7 @@ export default function CommandKClient({
|
|||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
case 'tags': return {
|
case 'tags': return {
|
||||||
heading: APP_TEXT.category.tagPlural,
|
heading: appText.category.tagPlural,
|
||||||
accessory: <IconTag
|
accessory: <IconTag
|
||||||
size={13}
|
size={13}
|
||||||
className="translate-x-[1px] translate-y-[0.75px]"
|
className="translate-x-[1px] translate-y-[0.75px]"
|
||||||
@ -337,7 +339,7 @@ export default function CommandKClient({
|
|||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
case 'recipes': return {
|
case 'recipes': return {
|
||||||
heading: APP_TEXT.category.recipePlural,
|
heading: appText.category.recipePlural,
|
||||||
accessory: <IconRecipe
|
accessory: <IconRecipe
|
||||||
size={15}
|
size={15}
|
||||||
className="translate-x-[-1px]"
|
className="translate-x-[-1px]"
|
||||||
@ -350,7 +352,7 @@ export default function CommandKClient({
|
|||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
case 'films': return {
|
case 'films': return {
|
||||||
heading: APP_TEXT.category.filmPlural,
|
heading: appText.category.filmPlural,
|
||||||
accessory: <IconFilm size={14} />,
|
accessory: <IconFilm size={14} />,
|
||||||
items: films.map(({ film, count }) => ({
|
items: films.map(({ film, count }) => ({
|
||||||
label: labelForFilm(film).medium,
|
label: labelForFilm(film).medium,
|
||||||
@ -360,7 +362,7 @@ export default function CommandKClient({
|
|||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
case 'focal-lengths': return {
|
case 'focal-lengths': return {
|
||||||
heading: APP_TEXT.category.focalLengthPlural,
|
heading: appText.category.focalLengthPlural,
|
||||||
accessory: <IconFocalLength className="text-[14px]" />,
|
accessory: <IconFocalLength className="text-[14px]" />,
|
||||||
items: focalLengths.map(({ focal, count }) => ({
|
items: focalLengths.map(({ focal, count }) => ({
|
||||||
label: formatFocalLength(focal)!,
|
label: formatFocalLength(focal)!,
|
||||||
@ -372,24 +374,32 @@ export default function CommandKClient({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.filter(Boolean) as CommandKSection[]
|
.filter(Boolean) as CommandKSection[]
|
||||||
, [tagsIncludingHidden, cameras, lenses, recipes, films, focalLengths]);
|
, [
|
||||||
|
appText,
|
||||||
|
tagsIncludingHidden,
|
||||||
|
cameras,
|
||||||
|
lenses,
|
||||||
|
recipes,
|
||||||
|
films,
|
||||||
|
focalLengths,
|
||||||
|
]);
|
||||||
|
|
||||||
const clientSections: CommandKSection[] = [{
|
const clientSections: CommandKSection[] = [{
|
||||||
heading: APP_TEXT.theme.theme,
|
heading: appText.theme.theme,
|
||||||
accessory: <IoInvertModeSharp
|
accessory: <IoInvertModeSharp
|
||||||
size={14}
|
size={14}
|
||||||
className="translate-y-[0.5px] translate-x-[-1px]"
|
className="translate-y-[0.5px] translate-x-[-1px]"
|
||||||
/>,
|
/>,
|
||||||
items: [{
|
items: [{
|
||||||
label: APP_TEXT.theme.system,
|
label: appText.theme.system,
|
||||||
annotation: <BiDesktop />,
|
annotation: <BiDesktop />,
|
||||||
action: () => setTheme('system'),
|
action: () => setTheme('system'),
|
||||||
}, {
|
}, {
|
||||||
label: APP_TEXT.theme.light,
|
label: appText.theme.light,
|
||||||
annotation: <BiSun size={16} className="translate-x-[1.25px]" />,
|
annotation: <BiSun size={16} className="translate-x-[1.25px]" />,
|
||||||
action: () => setTheme('light'),
|
action: () => setTheme('light'),
|
||||||
}, {
|
}, {
|
||||||
label: APP_TEXT.theme.dark,
|
label: appText.theme.dark,
|
||||||
annotation: <BiMoon className="translate-x-[1px]" />,
|
annotation: <BiMoon className="translate-x-[1px]" />,
|
||||||
action: () => setTheme('dark'),
|
action: () => setTheme('dark'),
|
||||||
}],
|
}],
|
||||||
@ -441,15 +451,15 @@ export default function CommandKClient({
|
|||||||
|
|
||||||
const pageFeed: CommandKItem = {
|
const pageFeed: CommandKItem = {
|
||||||
label: GRID_HOMEPAGE_ENABLED
|
label: GRID_HOMEPAGE_ENABLED
|
||||||
? APP_TEXT.nav.feed
|
? appText.nav.feed
|
||||||
: `${APP_TEXT.nav.feed} (${APP_TEXT.nav.home})`,
|
: `${appText.nav.feed} (${appText.nav.home})`,
|
||||||
path: PATH_FEED_INFERRED,
|
path: PATH_FEED_INFERRED,
|
||||||
};
|
};
|
||||||
|
|
||||||
const pageGrid: CommandKItem = {
|
const pageGrid: CommandKItem = {
|
||||||
label: GRID_HOMEPAGE_ENABLED
|
label: GRID_HOMEPAGE_ENABLED
|
||||||
? `${APP_TEXT.nav.grid} (${APP_TEXT.nav.home})`
|
? `${appText.nav.grid} (${appText.nav.home})`
|
||||||
: APP_TEXT.nav.grid,
|
: appText.nav.grid,
|
||||||
path: PATH_GRID_INFERRED,
|
path: PATH_GRID_INFERRED,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -471,40 +481,40 @@ export default function CommandKClient({
|
|||||||
|
|
||||||
if (isUserSignedIn) {
|
if (isUserSignedIn) {
|
||||||
adminSection.items.push({
|
adminSection.items.push({
|
||||||
label: APP_TEXT.admin.uploadPhotos,
|
label: appText.admin.uploadPhotos,
|
||||||
annotation: <IconLock narrow />,
|
annotation: <IconLock narrow />,
|
||||||
action: startUpload,
|
action: startUpload,
|
||||||
});
|
});
|
||||||
if (uploadsCount) {
|
if (uploadsCount) {
|
||||||
adminSection.items.push({
|
adminSection.items.push({
|
||||||
label: `${APP_TEXT.admin.uploadPlural} (${uploadsCount})`,
|
label: `${appText.admin.uploadPlural} (${uploadsCount})`,
|
||||||
annotation: <IconLock narrow />,
|
annotation: <IconLock narrow />,
|
||||||
path: PATH_ADMIN_UPLOADS,
|
path: PATH_ADMIN_UPLOADS,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
adminSection.items.push({
|
adminSection.items.push({
|
||||||
label: `${APP_TEXT.admin.managePhotos} (${photosCountTotal})`,
|
label: `${appText.admin.managePhotos} (${photosCountTotal})`,
|
||||||
annotation: <IconLock narrow />,
|
annotation: <IconLock narrow />,
|
||||||
path: PATH_ADMIN_PHOTOS,
|
path: PATH_ADMIN_PHOTOS,
|
||||||
});
|
});
|
||||||
if (tagsCount) {
|
if (tagsCount) {
|
||||||
adminSection.items.push({
|
adminSection.items.push({
|
||||||
label: `${APP_TEXT.admin.manageTags} (${tagsCount})`,
|
label: `${appText.admin.manageTags} (${tagsCount})`,
|
||||||
annotation: <IconLock narrow />,
|
annotation: <IconLock narrow />,
|
||||||
path: PATH_ADMIN_TAGS,
|
path: PATH_ADMIN_TAGS,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (recipesCount) {
|
if (recipesCount) {
|
||||||
adminSection.items.push({
|
adminSection.items.push({
|
||||||
label: `${APP_TEXT.admin.manageRecipes} (${recipesCount})`,
|
label: `${appText.admin.manageRecipes} (${recipesCount})`,
|
||||||
annotation: <IconLock narrow />,
|
annotation: <IconLock narrow />,
|
||||||
path: PATH_ADMIN_RECIPES,
|
path: PATH_ADMIN_RECIPES,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
adminSection.items.push({
|
adminSection.items.push({
|
||||||
label: selectedPhotoIds === undefined
|
label: selectedPhotoIds === undefined
|
||||||
? APP_TEXT.admin.batchEdit
|
? appText.admin.batchEdit
|
||||||
: APP_TEXT.admin.batchExitEdit,
|
: appText.admin.batchExitEdit,
|
||||||
annotation: <IconLock narrow />,
|
annotation: <IconLock narrow />,
|
||||||
path: selectedPhotoIds === undefined
|
path: selectedPhotoIds === undefined
|
||||||
? PATH_GRID_INFERRED
|
? PATH_GRID_INFERRED
|
||||||
@ -514,7 +524,7 @@ export default function CommandKClient({
|
|||||||
: () => setSelectedPhotoIds?.(undefined),
|
: () => setSelectedPhotoIds?.(undefined),
|
||||||
}, {
|
}, {
|
||||||
label: <span className="flex items-center gap-3">
|
label: <span className="flex items-center gap-3">
|
||||||
{APP_TEXT.admin.appInsights}
|
{appText.admin.appInsights}
|
||||||
{insightsIndicatorStatus &&
|
{insightsIndicatorStatus &&
|
||||||
<InsightsIndicatorDot />}
|
<InsightsIndicatorDot />}
|
||||||
</span>,
|
</span>,
|
||||||
@ -522,7 +532,7 @@ export default function CommandKClient({
|
|||||||
annotation: <IconLock narrow />,
|
annotation: <IconLock narrow />,
|
||||||
path: PATH_ADMIN_INSIGHTS,
|
path: PATH_ADMIN_INSIGHTS,
|
||||||
}, {
|
}, {
|
||||||
label: APP_TEXT.admin.appConfig,
|
label: appText.admin.appConfig,
|
||||||
annotation: <IconLock narrow />,
|
annotation: <IconLock narrow />,
|
||||||
path: PATH_ADMIN_CONFIGURATION,
|
path: PATH_ADMIN_CONFIGURATION,
|
||||||
});
|
});
|
||||||
@ -538,14 +548,14 @@ export default function CommandKClient({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
adminSection.items.push({
|
adminSection.items.push({
|
||||||
label: APP_TEXT.auth.signOut,
|
label: appText.auth.signOut,
|
||||||
action: () => signOutAction()
|
action: () => signOutAction()
|
||||||
.then(clearAuthStateAndRedirectIfNecessary)
|
.then(clearAuthStateAndRedirectIfNecessary)
|
||||||
.then(() => setIsOpen?.(false)),
|
.then(() => setIsOpen?.(false)),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
adminSection.items.push({
|
adminSection.items.push({
|
||||||
label: APP_TEXT.auth.signIn,
|
label: appText.auth.signIn,
|
||||||
path: PATH_SIGN_IN,
|
path: PATH_SIGN_IN,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -596,7 +606,7 @@ export default function CommandKClient({
|
|||||||
'focus:outline-hidden',
|
'focus:outline-hidden',
|
||||||
isPending && 'opacity-20',
|
isPending && 'opacity-20',
|
||||||
)}
|
)}
|
||||||
placeholder={APP_TEXT.cmdk.placeholder}
|
placeholder={appText.cmdk.placeholder}
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
/>
|
/>
|
||||||
{isLoading && !isPending &&
|
{isLoading && !isPending &&
|
||||||
@ -617,8 +627,8 @@ export default function CommandKClient({
|
|||||||
<div className="px-3 pt-2 pb-3.5 space-y-2">
|
<div className="px-3 pt-2 pb-3.5 space-y-2">
|
||||||
<Command.Empty className="mt-1 pl-3 text-dim text-base pb-0.5">
|
<Command.Empty className="mt-1 pl-3 text-dim text-base pb-0.5">
|
||||||
{isLoading
|
{isLoading
|
||||||
? APP_TEXT.cmdk.searching
|
? appText.cmdk.searching
|
||||||
: APP_TEXT.cmdk.noResults}
|
: appText.cmdk.noResults}
|
||||||
</Command.Empty>
|
</Command.Empty>
|
||||||
{queriedSections
|
{queriedSections
|
||||||
.concat(categorySections)
|
.concat(categorySections)
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import LoaderButton from './primitives/LoaderButton';
|
|||||||
import clsx from 'clsx/lite';
|
import clsx from 'clsx/lite';
|
||||||
import { toastSuccess } from '@/toast';
|
import { toastSuccess } from '@/toast';
|
||||||
import { ComponentProps } from 'react';
|
import { ComponentProps } from 'react';
|
||||||
import { APP_TEXT } from '@/app/config';
|
import { useAppText } from '@/i18n/state/client';
|
||||||
|
|
||||||
export default function CopyButton({
|
export default function CopyButton({
|
||||||
label,
|
label,
|
||||||
@ -19,6 +19,7 @@ export default function CopyButton({
|
|||||||
iconSize?: number
|
iconSize?: number
|
||||||
className?: string
|
className?: string
|
||||||
} & ComponentProps<typeof LoaderButton>) {
|
} & ComponentProps<typeof LoaderButton>) {
|
||||||
|
const appText = useAppText();
|
||||||
return (
|
return (
|
||||||
<LoaderButton
|
<LoaderButton
|
||||||
{...props}
|
{...props}
|
||||||
@ -30,7 +31,7 @@ export default function CopyButton({
|
|||||||
onClick={text
|
onClick={text
|
||||||
? () => {
|
? () => {
|
||||||
navigator.clipboard.writeText(text);
|
navigator.clipboard.writeText(text);
|
||||||
toastSuccess(APP_TEXT.misc.copyPhrase(label));
|
toastSuccess(appText.misc.copyPhrase(label));
|
||||||
}
|
}
|
||||||
: undefined}
|
: undefined}
|
||||||
styleAs="link"
|
styleAs="link"
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { downloadFileNameForPhoto, Photo } from '@/photo';
|
|||||||
import LoaderButton from './primitives/LoaderButton';
|
import LoaderButton from './primitives/LoaderButton';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { downloadFileFromBrowser } from '@/utility/url';
|
import { downloadFileFromBrowser } from '@/utility/url';
|
||||||
import { APP_TEXT } from '@/app/config';
|
import { useAppText } from '@/i18n/state/client';
|
||||||
|
|
||||||
export default function DownloadButton({
|
export default function DownloadButton({
|
||||||
photo,
|
photo,
|
||||||
@ -15,9 +15,11 @@ export default function DownloadButton({
|
|||||||
}) {
|
}) {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const appText = useAppText();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LoaderButton
|
<LoaderButton
|
||||||
tooltip={APP_TEXT.tooltip.download}
|
tooltip={appText.tooltip.download}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
className,
|
className,
|
||||||
'text-medium',
|
'text-medium',
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import { FiUploadCloud } from 'react-icons/fi';
|
|||||||
import { MAX_IMAGE_SIZE } from '@/platforms/next-image';
|
import { MAX_IMAGE_SIZE } from '@/platforms/next-image';
|
||||||
import ProgressButton from './primitives/ProgressButton';
|
import ProgressButton from './primitives/ProgressButton';
|
||||||
import { useAppState } from '@/state/AppState';
|
import { useAppState } from '@/state/AppState';
|
||||||
import { APP_TEXT } from '@/app/config';
|
import { useAppText } from '@/i18n/state/client';
|
||||||
|
|
||||||
export default function ImageInput({
|
export default function ImageInput({
|
||||||
ref: inputRefExternal,
|
ref: inputRefExternal,
|
||||||
@ -55,6 +55,8 @@ export default function ImageInput({
|
|||||||
resetUploadState,
|
resetUploadState,
|
||||||
} = useAppState();
|
} = useAppState();
|
||||||
|
|
||||||
|
const appText = useAppText();
|
||||||
|
|
||||||
const disabled = disabledProp || isUploading;
|
const disabled = disabledProp || isUploading;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -85,13 +87,13 @@ export default function ImageInput({
|
|||||||
>
|
>
|
||||||
{isUploading
|
{isUploading
|
||||||
? filesLength > 1
|
? filesLength > 1
|
||||||
? APP_TEXT.utility.paginateAction(
|
? appText.utility.paginateAction(
|
||||||
fileUploadIndex + 1,
|
fileUploadIndex + 1,
|
||||||
filesLength,
|
filesLength,
|
||||||
APP_TEXT.admin.uploading,
|
appText.admin.uploading,
|
||||||
)
|
)
|
||||||
: APP_TEXT.admin.uploading
|
: appText.admin.uploading
|
||||||
: APP_TEXT.admin.uploadPhotos}
|
: appText.admin.uploadPhotos}
|
||||||
</ProgressButton>}
|
</ProgressButton>}
|
||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
|
|||||||
@ -1,13 +1,15 @@
|
|||||||
import { APP_TEXT, TEMPLATE_REPO_NAME, TEMPLATE_REPO_URL } from '@/app/config';
|
import { TEMPLATE_REPO_NAME, TEMPLATE_REPO_URL } from '@/app/config';
|
||||||
|
import { useAppText } from '@/i18n/state/client';
|
||||||
import { clsx } from 'clsx/lite';
|
import { clsx } from 'clsx/lite';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { BiLogoGithub } from 'react-icons/bi';
|
import { BiLogoGithub } from 'react-icons/bi';
|
||||||
|
|
||||||
export default function RepoLink() {
|
export default function RepoLink() {
|
||||||
|
const appText = useAppText();
|
||||||
return (
|
return (
|
||||||
<span className="inline-flex items-center gap-2 whitespace-nowrap">
|
<span className="inline-flex items-center gap-2 whitespace-nowrap">
|
||||||
<span className="hidden sm:inline-block">
|
<span className="hidden sm:inline-block">
|
||||||
{APP_TEXT.misc.repo}
|
{appText.misc.repo}
|
||||||
</span>
|
</span>
|
||||||
<Link
|
<Link
|
||||||
href={TEMPLATE_REPO_URL}
|
href={TEMPLATE_REPO_URL}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { APP_TEXT } from '@/app/config';
|
import { useAppText } from '@/i18n/state/client';
|
||||||
import { toastWaiting } from '@/toast';
|
import { toastWaiting } from '@/toast';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useCallback, useEffect, useRef, useTransition } from 'react';
|
import { useCallback, useEffect, useRef, useTransition } from 'react';
|
||||||
@ -7,7 +7,7 @@ import { toast } from 'sonner';
|
|||||||
|
|
||||||
export default function useNavigateOrRunActionWithToast({
|
export default function useNavigateOrRunActionWithToast({
|
||||||
pathOrAction,
|
pathOrAction,
|
||||||
toastMessage = APP_TEXT.misc.loading,
|
toastMessage: _toastMessage,
|
||||||
dismissDelay = 1500,
|
dismissDelay = 1500,
|
||||||
}: {
|
}: {
|
||||||
pathOrAction?: string | (() => Promise<any> | undefined)
|
pathOrAction?: string | (() => Promise<any> | undefined)
|
||||||
@ -16,6 +16,10 @@ export default function useNavigateOrRunActionWithToast({
|
|||||||
}) {
|
}) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
const appText = useAppText();
|
||||||
|
|
||||||
|
const toastMessage = _toastMessage ?? appText.misc.loading;
|
||||||
|
|
||||||
const toastId = useRef<string | number>(undefined);
|
const toastId = useRef<string | number>(undefined);
|
||||||
|
|
||||||
const [isPending, startTransition] = useTransition();
|
const [isPending, startTransition] = useTransition();
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import PhotoFilm from '@/film/PhotoFilm';
|
|||||||
import { getRecipePropsFromPhotos } from '@/recipe';
|
import { getRecipePropsFromPhotos } from '@/recipe';
|
||||||
import { useAppState } from '@/state/AppState';
|
import { useAppState } from '@/state/AppState';
|
||||||
import { AI_TEXT_GENERATION_ENABLED } from '@/app/config';
|
import { AI_TEXT_GENERATION_ENABLED } from '@/app/config';
|
||||||
|
import { useAppText } from '@/i18n/state/client';
|
||||||
|
|
||||||
export default function FilmHeader({
|
export default function FilmHeader({
|
||||||
film,
|
film,
|
||||||
@ -31,6 +32,8 @@ export default function FilmHeader({
|
|||||||
? getRecipePropsFromPhotos(photos, selectedPhoto)
|
? getRecipePropsFromPhotos(photos, selectedPhoto)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
const appText = useAppText();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PhotoHeader
|
<PhotoHeader
|
||||||
film={film}
|
film={film}
|
||||||
@ -42,7 +45,12 @@ export default function FilmHeader({
|
|||||||
: undefined}
|
: undefined}
|
||||||
/>}
|
/>}
|
||||||
entityDescription={descriptionForFilmPhotos(
|
entityDescription={descriptionForFilmPhotos(
|
||||||
photos, undefined, count, dateRange)}
|
photos,
|
||||||
|
appText,
|
||||||
|
undefined,
|
||||||
|
count,
|
||||||
|
dateRange,
|
||||||
|
)}
|
||||||
photos={photos}
|
photos={photos}
|
||||||
selectedPhoto={selectedPhoto}
|
selectedPhoto={selectedPhoto}
|
||||||
indexNumber={indexNumber}
|
indexNumber={indexNumber}
|
||||||
|
|||||||
@ -5,8 +5,9 @@ import {
|
|||||||
} from '@/app/paths';
|
} from '@/app/paths';
|
||||||
import OGTile, { OGLoadingState } from '@/components/OGTile';
|
import OGTile, { OGLoadingState } from '@/components/OGTile';
|
||||||
import { descriptionForFilmPhotos, titleForFilm } from '.';
|
import { descriptionForFilmPhotos, titleForFilm } from '.';
|
||||||
|
import { getAppText } from '@/i18n/state/server';
|
||||||
|
|
||||||
export default function FilmOGTile({
|
export default async function FilmOGTile({
|
||||||
film,
|
film,
|
||||||
photos,
|
photos,
|
||||||
loadingState: loadingStateExternal,
|
loadingState: loadingStateExternal,
|
||||||
@ -27,11 +28,12 @@ export default function FilmOGTile({
|
|||||||
count?: number
|
count?: number
|
||||||
dateRange?: PhotoDateRange
|
dateRange?: PhotoDateRange
|
||||||
}) {
|
}) {
|
||||||
|
const appText = await getAppText();
|
||||||
return (
|
return (
|
||||||
<OGTile {...{
|
<OGTile {...{
|
||||||
title: titleForFilm(film, photos, count),
|
title: titleForFilm(film, photos, appText, count),
|
||||||
description:
|
description:
|
||||||
descriptionForFilmPhotos(photos, true, count, dateRange),
|
descriptionForFilmPhotos(photos, appText, true, count, dateRange),
|
||||||
path: pathForFilm(film),
|
path: pathForFilm(film),
|
||||||
pathImageAbsolute: absolutePathForFilmImage(film),
|
pathImageAbsolute: absolutePathForFilmImage(film),
|
||||||
loadingState: loadingStateExternal,
|
loadingState: loadingStateExternal,
|
||||||
|
|||||||
@ -3,8 +3,9 @@ import { PhotoSetAttributes } from '../category';
|
|||||||
import ShareModal from '@/share/ShareModal';
|
import ShareModal from '@/share/ShareModal';
|
||||||
import FilmOGTile from './FilmOGTile';
|
import FilmOGTile from './FilmOGTile';
|
||||||
import { labelForFilm, shareTextForFilm } from '.';
|
import { labelForFilm, shareTextForFilm } from '.';
|
||||||
|
import { getAppText } from '@/i18n/state/server';
|
||||||
|
|
||||||
export default function FilmShareModal({
|
export default async function FilmShareModal({
|
||||||
film,
|
film,
|
||||||
photos,
|
photos,
|
||||||
count,
|
count,
|
||||||
@ -12,11 +13,12 @@ export default function FilmShareModal({
|
|||||||
}: {
|
}: {
|
||||||
film: string
|
film: string
|
||||||
} & PhotoSetAttributes) {
|
} & PhotoSetAttributes) {
|
||||||
|
const appText = await getAppText();
|
||||||
return (
|
return (
|
||||||
<ShareModal
|
<ShareModal
|
||||||
pathShare={absolutePathForFilm(film, true)}
|
pathShare={absolutePathForFilm(film, true)}
|
||||||
navigatorTitle={labelForFilm(film).large}
|
navigatorTitle={labelForFilm(film).large}
|
||||||
socialText={shareTextForFilm(film)}
|
socialText={shareTextForFilm(film, appText)}
|
||||||
>
|
>
|
||||||
<FilmOGTile {...{ film, photos, count, dateRange }} />
|
<FilmOGTile {...{ film, photos, count, dateRange }} />
|
||||||
</ShareModal>
|
</ShareModal>
|
||||||
|
|||||||
@ -19,7 +19,7 @@ import {
|
|||||||
} from '@/utility/string';
|
} from '@/utility/string';
|
||||||
import { AnnotatedTag } from '@/photo/form';
|
import { AnnotatedTag } from '@/photo/form';
|
||||||
import PhotoFilmIcon from './PhotoFilmIcon';
|
import PhotoFilmIcon from './PhotoFilmIcon';
|
||||||
import { APP_TEXT } from '@/app/config';
|
import { I18NState } from '@/i18n/state';
|
||||||
|
|
||||||
export type FilmWithCount = {
|
export type FilmWithCount = {
|
||||||
film: string
|
film: string
|
||||||
@ -59,25 +59,29 @@ export const sortFilmsWithCount = (
|
|||||||
export const titleForFilm = (
|
export const titleForFilm = (
|
||||||
film: string,
|
film: string,
|
||||||
photos: Photo[],
|
photos: Photo[],
|
||||||
|
appText: I18NState,
|
||||||
explicitCount?: number,
|
explicitCount?: number,
|
||||||
) => [
|
) => [
|
||||||
labelForFilm(film).large,
|
labelForFilm(film).large,
|
||||||
photoQuantityText(explicitCount ?? photos.length),
|
photoQuantityText(explicitCount ?? photos.length, appText),
|
||||||
].join(' ');
|
].join(' ');
|
||||||
|
|
||||||
export const shareTextForFilm = (
|
export const shareTextForFilm = (
|
||||||
film: string,
|
film: string,
|
||||||
|
appText: I18NState,
|
||||||
) =>
|
) =>
|
||||||
APP_TEXT.category.filmShare(labelForFilm(film).large);
|
appText.category.filmShare(labelForFilm(film).large);
|
||||||
|
|
||||||
export const descriptionForFilmPhotos = (
|
export const descriptionForFilmPhotos = (
|
||||||
photos: Photo[],
|
photos: Photo[],
|
||||||
|
appText: I18NState,
|
||||||
dateBased?: boolean,
|
dateBased?: boolean,
|
||||||
explicitCount?: number,
|
explicitCount?: number,
|
||||||
explicitDateRange?: PhotoDateRange,
|
explicitDateRange?: PhotoDateRange,
|
||||||
) =>
|
) =>
|
||||||
descriptionForPhotoSet(
|
descriptionForPhotoSet(
|
||||||
photos,
|
photos,
|
||||||
|
appText,
|
||||||
undefined,
|
undefined,
|
||||||
dateBased,
|
dateBased,
|
||||||
explicitCount,
|
explicitCount,
|
||||||
@ -87,13 +91,15 @@ export const descriptionForFilmPhotos = (
|
|||||||
export const generateMetaForFilm = (
|
export const generateMetaForFilm = (
|
||||||
film: string,
|
film: string,
|
||||||
photos: Photo[],
|
photos: Photo[],
|
||||||
|
appText: I18NState,
|
||||||
explicitCount?: number,
|
explicitCount?: number,
|
||||||
explicitDateRange?: PhotoDateRange,
|
explicitDateRange?: PhotoDateRange,
|
||||||
) => ({
|
) => ({
|
||||||
url: absolutePathForFilm(film),
|
url: absolutePathForFilm(film),
|
||||||
title: titleForFilm(film, photos, explicitCount),
|
title: titleForFilm(film, photos, appText, explicitCount),
|
||||||
description: descriptionForFilmPhotos(
|
description: descriptionForFilmPhotos(
|
||||||
photos,
|
photos,
|
||||||
|
appText,
|
||||||
true,
|
true,
|
||||||
explicitCount,
|
explicitCount,
|
||||||
explicitDateRange,
|
explicitDateRange,
|
||||||
|
|||||||
@ -3,6 +3,8 @@ import { descriptionForFocalLengthPhotos } from '.';
|
|||||||
import PhotoHeader from '@/photo/PhotoHeader';
|
import PhotoHeader from '@/photo/PhotoHeader';
|
||||||
import PhotoFocalLength from './PhotoFocalLength';
|
import PhotoFocalLength from './PhotoFocalLength';
|
||||||
import { AI_TEXT_GENERATION_ENABLED } from '@/app/config';
|
import { AI_TEXT_GENERATION_ENABLED } from '@/app/config';
|
||||||
|
import { useAppText } from '@/i18n/state/client';
|
||||||
|
|
||||||
export default function FocalLengthHeader({
|
export default function FocalLengthHeader({
|
||||||
focal,
|
focal,
|
||||||
photos,
|
photos,
|
||||||
@ -18,14 +20,17 @@ export default function FocalLengthHeader({
|
|||||||
count?: number
|
count?: number
|
||||||
dateRange?: PhotoDateRange
|
dateRange?: PhotoDateRange
|
||||||
}) {
|
}) {
|
||||||
|
const appText = useAppText();
|
||||||
return (
|
return (
|
||||||
<PhotoHeader
|
<PhotoHeader
|
||||||
focal={focal}
|
focal={focal}
|
||||||
entity={<PhotoFocalLength focal={focal} contrast="high" />}
|
entity={<PhotoFocalLength focal={focal} contrast="high" />}
|
||||||
entityDescription={descriptionForFocalLengthPhotos(
|
entityDescription={descriptionForFocalLengthPhotos(
|
||||||
photos,
|
photos,
|
||||||
|
appText,
|
||||||
undefined,
|
undefined,
|
||||||
count,
|
count,
|
||||||
|
dateRange,
|
||||||
)}
|
)}
|
||||||
photos={photos}
|
photos={photos}
|
||||||
selectedPhoto={selectedPhoto}
|
selectedPhoto={selectedPhoto}
|
||||||
|
|||||||
@ -5,8 +5,9 @@ import {
|
|||||||
} from '@/app/paths';
|
} from '@/app/paths';
|
||||||
import OGTile, { OGLoadingState } from '@/components/OGTile';
|
import OGTile, { OGLoadingState } from '@/components/OGTile';
|
||||||
import { descriptionForFocalLengthPhotos, titleForFocalLength } from '.';
|
import { descriptionForFocalLengthPhotos, titleForFocalLength } from '.';
|
||||||
|
import { getAppText } from '@/i18n/state/server';
|
||||||
|
|
||||||
export default function FocalLengthOGTile({
|
export default async function FocalLengthOGTile({
|
||||||
focal,
|
focal,
|
||||||
photos,
|
photos,
|
||||||
loadingState: loadingStateExternal,
|
loadingState: loadingStateExternal,
|
||||||
@ -27,11 +28,13 @@ export default function FocalLengthOGTile({
|
|||||||
count?: number
|
count?: number
|
||||||
dateRange?: PhotoDateRange
|
dateRange?: PhotoDateRange
|
||||||
}) {
|
}) {
|
||||||
|
const appText = await getAppText();
|
||||||
return (
|
return (
|
||||||
<OGTile {...{
|
<OGTile {...{
|
||||||
title: titleForFocalLength(focal, photos, count),
|
title: titleForFocalLength(focal, photos, appText, count),
|
||||||
description: descriptionForFocalLengthPhotos(
|
description: descriptionForFocalLengthPhotos(
|
||||||
photos,
|
photos,
|
||||||
|
appText,
|
||||||
true,
|
true,
|
||||||
count,
|
count,
|
||||||
dateRange,
|
dateRange,
|
||||||
|
|||||||
@ -3,8 +3,9 @@ import { PhotoSetAttributes } from '../category';
|
|||||||
import ShareModal from '@/share/ShareModal';
|
import ShareModal from '@/share/ShareModal';
|
||||||
import FocalLengthOGTile from './FocalLengthOGTile';
|
import FocalLengthOGTile from './FocalLengthOGTile';
|
||||||
import { formatFocalLengthSafe, shareTextFocalLength } from '.';
|
import { formatFocalLengthSafe, shareTextFocalLength } from '.';
|
||||||
|
import { getAppText } from '@/i18n/state/server';
|
||||||
|
|
||||||
export default function FocalLengthShareModal({
|
export default async function FocalLengthShareModal({
|
||||||
focal,
|
focal,
|
||||||
photos,
|
photos,
|
||||||
count,
|
count,
|
||||||
@ -12,11 +13,12 @@ export default function FocalLengthShareModal({
|
|||||||
}: {
|
}: {
|
||||||
focal: number
|
focal: number
|
||||||
} & PhotoSetAttributes) {
|
} & PhotoSetAttributes) {
|
||||||
|
const appText = await getAppText();
|
||||||
return (
|
return (
|
||||||
<ShareModal
|
<ShareModal
|
||||||
pathShare={absolutePathForFocalLength(focal, true)}
|
pathShare={absolutePathForFocalLength(focal, true)}
|
||||||
navigatorTitle={formatFocalLengthSafe(focal)}
|
navigatorTitle={formatFocalLengthSafe(focal)}
|
||||||
socialText={shareTextFocalLength(focal)}
|
socialText={shareTextFocalLength(focal, appText)}
|
||||||
>
|
>
|
||||||
<FocalLengthOGTile {...{ focal, photos, count, dateRange }} />
|
<FocalLengthOGTile {...{ focal, photos, count, dateRange }} />
|
||||||
</ShareModal>
|
</ShareModal>
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import {
|
|||||||
absolutePathForFocalLength,
|
absolutePathForFocalLength,
|
||||||
absolutePathForFocalLengthImage,
|
absolutePathForFocalLengthImage,
|
||||||
} from '@/app/paths';
|
} from '@/app/paths';
|
||||||
import { APP_TEXT } from '@/app/config';
|
import { I18NState } from '@/i18n/state';
|
||||||
|
|
||||||
export type FocalLengths = {
|
export type FocalLengths = {
|
||||||
focal: number
|
focal: number
|
||||||
@ -30,23 +30,29 @@ export const formatFocalLengthSafe = (focal = 0) =>
|
|||||||
export const titleForFocalLength = (
|
export const titleForFocalLength = (
|
||||||
focal: number,
|
focal: number,
|
||||||
photos: Photo[],
|
photos: Photo[],
|
||||||
|
appText: I18NState,
|
||||||
explicitCount?: number,
|
explicitCount?: number,
|
||||||
) => [
|
) => [
|
||||||
APP_TEXT.category.focalLengthTitle(formatFocalLengthSafe(focal)),
|
appText.category.focalLengthTitle(formatFocalLengthSafe(focal)),
|
||||||
photoQuantityText(explicitCount ?? photos.length),
|
photoQuantityText(explicitCount ?? photos.length, appText),
|
||||||
].join(' ');
|
].join(' ');
|
||||||
|
|
||||||
export const shareTextFocalLength = (focal: number) =>
|
export const shareTextFocalLength = (
|
||||||
APP_TEXT.category.focalLengthShare(formatFocalLengthSafe(focal));
|
focal: number,
|
||||||
|
appText: I18NState,
|
||||||
|
) =>
|
||||||
|
appText.category.focalLengthShare(formatFocalLengthSafe(focal));
|
||||||
|
|
||||||
export const descriptionForFocalLengthPhotos = (
|
export const descriptionForFocalLengthPhotos = (
|
||||||
photos: Photo[],
|
photos: Photo[],
|
||||||
|
appText: I18NState,
|
||||||
dateBased?: boolean,
|
dateBased?: boolean,
|
||||||
explicitCount?: number,
|
explicitCount?: number,
|
||||||
explicitDateRange?: PhotoDateRange,
|
explicitDateRange?: PhotoDateRange,
|
||||||
) =>
|
) =>
|
||||||
descriptionForPhotoSet(
|
descriptionForPhotoSet(
|
||||||
photos,
|
photos,
|
||||||
|
appText,
|
||||||
undefined,
|
undefined,
|
||||||
dateBased,
|
dateBased,
|
||||||
explicitCount,
|
explicitCount,
|
||||||
@ -56,13 +62,15 @@ export const descriptionForFocalLengthPhotos = (
|
|||||||
export const generateMetaForFocalLength = (
|
export const generateMetaForFocalLength = (
|
||||||
focal: number,
|
focal: number,
|
||||||
photos: Photo[],
|
photos: Photo[],
|
||||||
|
appText: I18NState,
|
||||||
explicitCount?: number,
|
explicitCount?: number,
|
||||||
explicitDateRange?: PhotoDateRange,
|
explicitDateRange?: PhotoDateRange,
|
||||||
) => ({
|
) => ({
|
||||||
url: absolutePathForFocalLength(focal),
|
url: absolutePathForFocalLength(focal),
|
||||||
title: titleForFocalLength(focal, photos, explicitCount),
|
title: titleForFocalLength(focal, photos, appText, explicitCount),
|
||||||
description: descriptionForFocalLengthPhotos(
|
description: descriptionForFocalLengthPhotos(
|
||||||
photos,
|
photos,
|
||||||
|
appText,
|
||||||
true,
|
true,
|
||||||
explicitCount,
|
explicitCount,
|
||||||
explicitDateRange,
|
explicitDateRange,
|
||||||
|
|||||||
12
src/i18n/date.ts
Normal file
12
src/i18n/date.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { enUS, ptBR, pt } from 'date-fns/locale';
|
||||||
|
import { APP_LOCALE } from '@/app/config';
|
||||||
|
|
||||||
|
const getDateFnLocale = (locale: string) => {
|
||||||
|
switch (locale) {
|
||||||
|
case 'pt-pt': return pt;
|
||||||
|
case 'pt-br': return ptBR;
|
||||||
|
default: return enUS;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DATE_FN_LOCALE = getDateFnLocale(APP_LOCALE);
|
||||||
@ -1,7 +1,4 @@
|
|||||||
import US_EN from './locales/us-en';
|
import US_EN from './locales/us-en';
|
||||||
import PT_BR from './locales/pt-br';
|
|
||||||
import PT_PT from './locales/pt-pt';
|
|
||||||
import { enUS, ptBR, pt } from 'date-fns/locale';
|
|
||||||
|
|
||||||
export type I18N = typeof US_EN;
|
export type I18N = typeof US_EN;
|
||||||
|
|
||||||
@ -9,71 +6,20 @@ export type I18NDeepPartial = {
|
|||||||
[key in keyof I18N]?: Partial<I18N[key]>;
|
[key in keyof I18N]?: Partial<I18N[key]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDateFnLocale = (locale: string) => {
|
export const LOCALE_TEXT: Record<
|
||||||
switch (locale) {
|
string,
|
||||||
case 'pt-pt': return pt;
|
() => Promise<I18NDeepPartial | undefined>
|
||||||
case 'pt-br': return ptBR;
|
> = {
|
||||||
default: return enUS;
|
'pt-br': () => import('./locales/pt-br').then((m) => m.default),
|
||||||
}
|
'pt-pt': () => import('./locales/pt-pt').then((m) => m.default),
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateI18NWithFunctions = (i18nText: I18N) => {
|
export const getTextForLocale = async (
|
||||||
return {
|
|
||||||
...i18nText,
|
|
||||||
category: {
|
|
||||||
...i18nText.category,
|
|
||||||
cameraTitle: (camera: string) =>
|
|
||||||
i18nText.category.cameraTitle.replace('{{camera}}', camera),
|
|
||||||
cameraShare: (camera: string) =>
|
|
||||||
i18nText.category.cameraShare.replace('{{camera}}', camera),
|
|
||||||
taggedPhrase: (tag: string) =>
|
|
||||||
i18nText.category.taggedPhrase.replace('{{tag}}', tag),
|
|
||||||
recipeShare: (recipe: string) =>
|
|
||||||
i18nText.category.recipeShare.replace('{{recipe}}', recipe),
|
|
||||||
filmShare: (film: string) =>
|
|
||||||
i18nText.category.filmShare.replace('{{film}}', film),
|
|
||||||
focalLengthTitle: (focal: string) =>
|
|
||||||
i18nText.category.focalLengthTitle.replace('{{focal}}', focal),
|
|
||||||
focalLengthShare: (focal: string) =>
|
|
||||||
i18nText.category.focalLengthShare.replace('{{focal}}', focal),
|
|
||||||
},
|
|
||||||
admin: {
|
|
||||||
...i18nText.admin,
|
|
||||||
deleteConfirm: (photoTitle: string) =>
|
|
||||||
i18nText.admin.deleteConfirm.replace('{{photoTitle}}', photoTitle),
|
|
||||||
},
|
|
||||||
misc: {
|
|
||||||
...i18nText.misc,
|
|
||||||
copyPhrase: (label: string) =>
|
|
||||||
i18nText.misc.copyPhrase.replace('{{label}}', label),
|
|
||||||
},
|
|
||||||
utility: {
|
|
||||||
...i18nText.utility,
|
|
||||||
paginate: (index: number, count: number) =>
|
|
||||||
i18nText.utility.paginate
|
|
||||||
.replace('{{index}}', index.toString())
|
|
||||||
.replace('{{count}}', count.toString()),
|
|
||||||
paginateAction: (index: number, count: number, action: string) =>
|
|
||||||
i18nText.utility.paginateAction
|
|
||||||
.replace('{{index}}', index.toString())
|
|
||||||
.replace('{{count}}', count.toString())
|
|
||||||
.replace('{{action}}', action),
|
|
||||||
},
|
|
||||||
dateLocale: getDateFnLocale(i18nText.locale),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const LOCALE_TEXT: Record<string, I18NDeepPartial | undefined> = {
|
|
||||||
'pt-br': PT_BR,
|
|
||||||
'pt-pt': PT_PT,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getTextForLocale = (
|
|
||||||
locale = '',
|
locale = '',
|
||||||
) => {
|
): Promise<I18N> => {
|
||||||
const text = US_EN;
|
const text = US_EN;
|
||||||
|
|
||||||
Object.entries(LOCALE_TEXT[locale.toLocaleLowerCase()] ?? {})
|
Object.entries(await LOCALE_TEXT[locale.toLocaleLowerCase()]?.() ?? {})
|
||||||
.forEach(([key, value]) => {
|
.forEach(([key, value]) => {
|
||||||
// Fall back to English for missing keys
|
// Fall back to English for missing keys
|
||||||
text[key as keyof I18N] = {
|
text[key as keyof I18N] = {
|
||||||
@ -82,5 +28,5 @@ export const getTextForLocale = (
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return generateI18NWithFunctions(text);
|
return text;
|
||||||
};
|
};
|
||||||
|
|||||||
17
src/i18n/state/AppTextProvider.tsx
Normal file
17
src/i18n/state/AppTextProvider.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { ReactNode } from 'react';
|
||||||
|
import { getTextForLocale } from '..';
|
||||||
|
import { APP_LOCALE } from '@/app/config';
|
||||||
|
import AppTextProviderClient from './AppTextProviderClient';
|
||||||
|
|
||||||
|
export default async function AppTextProvider({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: ReactNode
|
||||||
|
}) {
|
||||||
|
const value = await getTextForLocale(APP_LOCALE);
|
||||||
|
return (
|
||||||
|
<AppTextProviderClient {...{ value }}>
|
||||||
|
{children}
|
||||||
|
</AppTextProviderClient>
|
||||||
|
);
|
||||||
|
}
|
||||||
20
src/i18n/state/AppTextProviderClient.tsx
Normal file
20
src/i18n/state/AppTextProviderClient.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
import { AppTextContext } from './client';
|
||||||
|
import { I18N } from '..';
|
||||||
|
import { generateI18NState } from '.';
|
||||||
|
|
||||||
|
export default function AppTextProviderClient({
|
||||||
|
children,
|
||||||
|
value,
|
||||||
|
}: {
|
||||||
|
children: ReactNode
|
||||||
|
value: I18N
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<AppTextContext.Provider value={generateI18NState(value)}>
|
||||||
|
{children}
|
||||||
|
</AppTextContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
9
src/i18n/state/client.ts
Normal file
9
src/i18n/state/client.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { createContext, use } from 'react';
|
||||||
|
import { generateI18NState } from '.';
|
||||||
|
import US_EN from '../locales/us-en';
|
||||||
|
|
||||||
|
export const AppTextContext = createContext(generateI18NState(US_EN));
|
||||||
|
|
||||||
|
export const useAppText = () => use(AppTextContext);
|
||||||
48
src/i18n/state/index.ts
Normal file
48
src/i18n/state/index.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { I18N } from '..';
|
||||||
|
|
||||||
|
export type I18NState = ReturnType<typeof generateI18NState>;
|
||||||
|
|
||||||
|
export const generateI18NState = (i18nText: I18N) => {
|
||||||
|
return {
|
||||||
|
...i18nText,
|
||||||
|
category: {
|
||||||
|
...i18nText.category,
|
||||||
|
cameraTitle: (camera: string) =>
|
||||||
|
i18nText.category.cameraTitle.replace('{{camera}}', camera),
|
||||||
|
cameraShare: (camera: string) =>
|
||||||
|
i18nText.category.cameraShare.replace('{{camera}}', camera),
|
||||||
|
taggedPhrase: (tag: string) =>
|
||||||
|
i18nText.category.taggedPhrase.replace('{{tag}}', tag),
|
||||||
|
recipeShare: (recipe: string) =>
|
||||||
|
i18nText.category.recipeShare.replace('{{recipe}}', recipe),
|
||||||
|
filmShare: (film: string) =>
|
||||||
|
i18nText.category.filmShare.replace('{{film}}', film),
|
||||||
|
focalLengthTitle: (focal: string) =>
|
||||||
|
i18nText.category.focalLengthTitle.replace('{{focal}}', focal),
|
||||||
|
focalLengthShare: (focal: string) =>
|
||||||
|
i18nText.category.focalLengthShare.replace('{{focal}}', focal),
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
...i18nText.admin,
|
||||||
|
deleteConfirm: (photoTitle: string) =>
|
||||||
|
i18nText.admin.deleteConfirm.replace('{{photoTitle}}', photoTitle),
|
||||||
|
},
|
||||||
|
misc: {
|
||||||
|
...i18nText.misc,
|
||||||
|
copyPhrase: (label: string) =>
|
||||||
|
i18nText.misc.copyPhrase.replace('{{label}}', label),
|
||||||
|
},
|
||||||
|
utility: {
|
||||||
|
...i18nText.utility,
|
||||||
|
paginate: (index: number, count: number) =>
|
||||||
|
i18nText.utility.paginate
|
||||||
|
.replace('{{index}}', index.toString())
|
||||||
|
.replace('{{count}}', count.toString()),
|
||||||
|
paginateAction: (index: number, count: number, action: string) =>
|
||||||
|
i18nText.utility.paginateAction
|
||||||
|
.replace('{{index}}', index.toString())
|
||||||
|
.replace('{{count}}', count.toString())
|
||||||
|
.replace('{{action}}', action),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
6
src/i18n/state/server.ts
Normal file
6
src/i18n/state/server.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { APP_LOCALE } from '@/app/config';
|
||||||
|
import { getTextForLocale } from '..';
|
||||||
|
import { generateI18NState } from '.';
|
||||||
|
|
||||||
|
export const getAppText = async () =>
|
||||||
|
getTextForLocale(APP_LOCALE).then(generateI18NState);
|
||||||
@ -4,8 +4,9 @@ import { Lens, lensFromPhoto } from '.';
|
|||||||
import PhotoLens from './PhotoLens';
|
import PhotoLens from './PhotoLens';
|
||||||
import { descriptionForLensPhotos } from './meta';
|
import { descriptionForLensPhotos } from './meta';
|
||||||
import { AI_TEXT_GENERATION_ENABLED } from '@/app/config';
|
import { AI_TEXT_GENERATION_ENABLED } from '@/app/config';
|
||||||
|
import { getAppText } from '@/i18n/state/server';
|
||||||
|
|
||||||
export default function LensHeader({
|
export default async function LensHeader({
|
||||||
lens: lensProp,
|
lens: lensProp,
|
||||||
photos,
|
photos,
|
||||||
selectedPhoto,
|
selectedPhoto,
|
||||||
@ -21,12 +22,19 @@ export default function LensHeader({
|
|||||||
dateRange?: PhotoDateRange
|
dateRange?: PhotoDateRange
|
||||||
}) {
|
}) {
|
||||||
const lens = lensFromPhoto(photos[0], lensProp);
|
const lens = lensFromPhoto(photos[0], lensProp);
|
||||||
|
const appText = await getAppText();
|
||||||
return (
|
return (
|
||||||
<PhotoHeader
|
<PhotoHeader
|
||||||
lens={lens}
|
lens={lens}
|
||||||
entity={<PhotoLens {...{ lens }} contrast="high" />}
|
entity={<PhotoLens {...{ lens }} contrast="high" />}
|
||||||
entityDescription={
|
entityDescription={
|
||||||
descriptionForLensPhotos(photos, undefined, count, dateRange)}
|
descriptionForLensPhotos(
|
||||||
|
photos,
|
||||||
|
appText,
|
||||||
|
undefined,
|
||||||
|
count,
|
||||||
|
dateRange,
|
||||||
|
)}
|
||||||
photos={photos}
|
photos={photos}
|
||||||
selectedPhoto={selectedPhoto}
|
selectedPhoto={selectedPhoto}
|
||||||
indexNumber={indexNumber}
|
indexNumber={indexNumber}
|
||||||
|
|||||||
@ -3,8 +3,9 @@ import { absolutePathForLensImage, pathForLens } from '@/app/paths';
|
|||||||
import OGTile, { OGLoadingState } from '@/components/OGTile';
|
import OGTile, { OGLoadingState } from '@/components/OGTile';
|
||||||
import { Lens } from '.';
|
import { Lens } from '.';
|
||||||
import { titleForLens, descriptionForLensPhotos } from './meta';
|
import { titleForLens, descriptionForLensPhotos } from './meta';
|
||||||
|
import { getAppText } from '@/i18n/state/server';
|
||||||
|
|
||||||
export default function LensOGTile({
|
export default async function LensOGTile({
|
||||||
lens,
|
lens,
|
||||||
photos,
|
photos,
|
||||||
loadingState: loadingStateExternal,
|
loadingState: loadingStateExternal,
|
||||||
@ -25,10 +26,17 @@ export default function LensOGTile({
|
|||||||
count?: number
|
count?: number
|
||||||
dateRange?: PhotoDateRange
|
dateRange?: PhotoDateRange
|
||||||
}) {
|
}) {
|
||||||
|
const appText = await getAppText();
|
||||||
return (
|
return (
|
||||||
<OGTile {...{
|
<OGTile {...{
|
||||||
title: titleForLens(lens, photos, count),
|
title: titleForLens(lens, photos, appText, count),
|
||||||
description: descriptionForLensPhotos(photos, true, count, dateRange),
|
description: descriptionForLensPhotos(
|
||||||
|
photos,
|
||||||
|
appText,
|
||||||
|
true,
|
||||||
|
count,
|
||||||
|
dateRange,
|
||||||
|
),
|
||||||
path: pathForLens(lens),
|
path: pathForLens(lens),
|
||||||
pathImageAbsolute: absolutePathForLensImage(lens),
|
pathImageAbsolute: absolutePathForLensImage(lens),
|
||||||
loadingState: loadingStateExternal,
|
loadingState: loadingStateExternal,
|
||||||
|
|||||||
@ -4,8 +4,9 @@ import ShareModal from '@/share/ShareModal';
|
|||||||
import { formatLensText, Lens } from '.';
|
import { formatLensText, Lens } from '.';
|
||||||
import { shareTextForLens } from './meta';
|
import { shareTextForLens } from './meta';
|
||||||
import LensOGTile from './LensOGTile';
|
import LensOGTile from './LensOGTile';
|
||||||
|
import { getAppText } from '@/i18n/state/server';
|
||||||
|
|
||||||
export default function LensShareModal({
|
export default async function LensShareModal({
|
||||||
lens,
|
lens,
|
||||||
photos,
|
photos,
|
||||||
count,
|
count,
|
||||||
@ -13,11 +14,12 @@ export default function LensShareModal({
|
|||||||
}: {
|
}: {
|
||||||
lens: Lens
|
lens: Lens
|
||||||
} & PhotoSetAttributes) {
|
} & PhotoSetAttributes) {
|
||||||
|
const appText = await getAppText();
|
||||||
return (
|
return (
|
||||||
<ShareModal
|
<ShareModal
|
||||||
pathShare={absolutePathForLens(lens, true)}
|
pathShare={absolutePathForLens(lens, true)}
|
||||||
navigatorTitle={formatLensText(lens)}
|
navigatorTitle={formatLensText(lens)}
|
||||||
socialText={shareTextForLens(lens, photos)}
|
socialText={shareTextForLens(lens, photos, appText)}
|
||||||
>
|
>
|
||||||
<LensOGTile {...{ lens, photos, count, dateRange }} />
|
<LensOGTile {...{ lens, photos, count, dateRange }} />
|
||||||
</ShareModal>
|
</ShareModal>
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import {
|
|||||||
absolutePathForLens,
|
absolutePathForLens,
|
||||||
absolutePathForLensImage,
|
absolutePathForLensImage,
|
||||||
} from '@/app/paths';
|
} from '@/app/paths';
|
||||||
import { APP_TEXT } from '@/app/config';
|
import { I18NState } from '@/i18n/state';
|
||||||
|
|
||||||
// Meta functions moved to separate file to avoid
|
// Meta functions moved to separate file to avoid
|
||||||
// dependencies (camelcase-keys) found in photo/index.ts
|
// dependencies (camelcase-keys) found in photo/index.ts
|
||||||
@ -18,30 +18,34 @@ import { APP_TEXT } from '@/app/config';
|
|||||||
export const titleForLens = (
|
export const titleForLens = (
|
||||||
lens: Lens,
|
lens: Lens,
|
||||||
photos: Photo[],
|
photos: Photo[],
|
||||||
|
appText: I18NState,
|
||||||
explicitCount?: number,
|
explicitCount?: number,
|
||||||
) => [
|
) => [
|
||||||
`${APP_TEXT.category.lens}:`,
|
`${appText.category.lens}:`,
|
||||||
formatLensText(lensFromPhoto(photos[0], lens)),
|
formatLensText(lensFromPhoto(photos[0], lens)),
|
||||||
photoQuantityText(explicitCount ?? photos.length),
|
photoQuantityText(explicitCount ?? photos.length, appText),
|
||||||
].join(' ');
|
].join(' ');
|
||||||
|
|
||||||
export const shareTextForLens = (
|
export const shareTextForLens = (
|
||||||
lens: Lens,
|
lens: Lens,
|
||||||
photos: Photo[],
|
photos: Photo[],
|
||||||
|
appText: I18NState,
|
||||||
) =>
|
) =>
|
||||||
[
|
[
|
||||||
`${APP_TEXT.category.lens}:`,
|
`${appText.category.lens}:`,
|
||||||
formatLensText(lensFromPhoto(photos[0], lens)),
|
formatLensText(lensFromPhoto(photos[0], lens)),
|
||||||
].join(' ');
|
].join(' ');
|
||||||
|
|
||||||
export const descriptionForLensPhotos = (
|
export const descriptionForLensPhotos = (
|
||||||
photos: Photo[],
|
photos: Photo[],
|
||||||
|
appText: I18NState,
|
||||||
dateBased?: boolean,
|
dateBased?: boolean,
|
||||||
explicitCount?: number,
|
explicitCount?: number,
|
||||||
explicitDateRange?: PhotoDateRange,
|
explicitDateRange?: PhotoDateRange,
|
||||||
) =>
|
) =>
|
||||||
descriptionForPhotoSet(
|
descriptionForPhotoSet(
|
||||||
photos,
|
photos,
|
||||||
|
appText,
|
||||||
undefined,
|
undefined,
|
||||||
dateBased,
|
dateBased,
|
||||||
explicitCount,
|
explicitCount,
|
||||||
@ -51,12 +55,19 @@ export const descriptionForLensPhotos = (
|
|||||||
export const generateMetaForLens = (
|
export const generateMetaForLens = (
|
||||||
lens: Lens,
|
lens: Lens,
|
||||||
photos: Photo[],
|
photos: Photo[],
|
||||||
|
appText: I18NState,
|
||||||
explicitCount?: number,
|
explicitCount?: number,
|
||||||
explicitDateRange?: PhotoDateRange,
|
explicitDateRange?: PhotoDateRange,
|
||||||
) => ({
|
) => ({
|
||||||
url: absolutePathForLens(lens),
|
url: absolutePathForLens(lens),
|
||||||
title: titleForLens(lens, photos, explicitCount),
|
title: titleForLens(lens, photos, appText, explicitCount),
|
||||||
description:
|
description:
|
||||||
descriptionForLensPhotos(photos, true, explicitCount, explicitDateRange),
|
descriptionForLensPhotos(
|
||||||
|
photos,
|
||||||
|
appText,
|
||||||
|
true,
|
||||||
|
explicitCount,
|
||||||
|
explicitDateRange,
|
||||||
|
),
|
||||||
images: absolutePathForLensImage(lens),
|
images: absolutePathForLensImage(lens),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import ResponsiveDate from '@/components/ResponsiveDate';
|
|||||||
import { Photo } from '.';
|
import { Photo } from '.';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { Timezone } from '@/utility/timezone';
|
import { Timezone } from '@/utility/timezone';
|
||||||
import { APP_TEXT } from '@/app/config';
|
import { useAppText } from '@/i18n/state/client';
|
||||||
|
|
||||||
export default function PhotoDate({
|
export default function PhotoDate({
|
||||||
photo,
|
photo,
|
||||||
@ -31,14 +31,16 @@ export default function PhotoDate({
|
|||||||
photo.updatedAt,
|
photo.updatedAt,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const appText = useAppText();
|
||||||
|
|
||||||
const getTitleLabel = () => {
|
const getTitleLabel = () => {
|
||||||
switch (dateType) {
|
switch (dateType) {
|
||||||
case 'takenAt':
|
case 'takenAt':
|
||||||
return APP_TEXT.photo.taken;
|
return appText.photo.taken;
|
||||||
case 'createdAt':
|
case 'createdAt':
|
||||||
return APP_TEXT.photo.created;
|
return appText.photo.created;
|
||||||
case 'updatedAt':
|
case 'updatedAt':
|
||||||
return APP_TEXT.photo.updated;
|
return appText.photo.updated;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import FavsTag from '../tag/FavsTag';
|
|||||||
import { useAppState } from '@/state/AppState';
|
import { useAppState } from '@/state/AppState';
|
||||||
import { useMemo, useRef } from 'react';
|
import { useMemo, useRef } from 'react';
|
||||||
import HiddenTag from '@/tag/HiddenTag';
|
import HiddenTag from '@/tag/HiddenTag';
|
||||||
import { APP_TEXT, CATEGORY_VISIBILITY } from '@/app/config';
|
import { CATEGORY_VISIBILITY } from '@/app/config';
|
||||||
import { clsx } from 'clsx/lite';
|
import { clsx } from 'clsx/lite';
|
||||||
import PhotoRecipe from '@/recipe/PhotoRecipe';
|
import PhotoRecipe from '@/recipe/PhotoRecipe';
|
||||||
import IconCamera from '@/components/icons/IconCamera';
|
import IconCamera from '@/components/icons/IconCamera';
|
||||||
@ -26,6 +26,7 @@ import {
|
|||||||
} from '@/category';
|
} from '@/category';
|
||||||
import PhotoFocalLength from '@/focal/PhotoFocalLength';
|
import PhotoFocalLength from '@/focal/PhotoFocalLength';
|
||||||
import useElementHeight from '@/utility/useElementHeight';
|
import useElementHeight from '@/utility/useElementHeight';
|
||||||
|
import { useAppText } from '@/i18n/state/client';
|
||||||
|
|
||||||
const APPROXIMATE_ITEM_HEIGHT = 34;
|
const APPROXIMATE_ITEM_HEIGHT = 34;
|
||||||
const ABOUT_HEIGHT_OFFSET = 80;
|
const ABOUT_HEIGHT_OFFSET = 80;
|
||||||
@ -58,6 +59,8 @@ export default function PhotoGridSidebar({
|
|||||||
categories,
|
categories,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const appText = useAppText();
|
||||||
|
|
||||||
const aboutRef = useRef<HTMLParagraphElement>(null);
|
const aboutRef = useRef<HTMLParagraphElement>(null);
|
||||||
const aboutHeight = useElementHeight(aboutRef);
|
const aboutHeight = useElementHeight(aboutRef);
|
||||||
const height = containerHeight
|
const height = containerHeight
|
||||||
@ -72,7 +75,10 @@ export default function PhotoGridSidebar({
|
|||||||
)
|
)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const { start, end } = dateRangeForPhotos(undefined, photosDateRange);
|
const { start, end } = dateRangeForPhotos(
|
||||||
|
undefined,
|
||||||
|
photosDateRange,
|
||||||
|
);
|
||||||
|
|
||||||
const { photosCountHidden } = useAppState();
|
const { photosCountHidden } = useAppState();
|
||||||
|
|
||||||
@ -83,7 +89,7 @@ export default function PhotoGridSidebar({
|
|||||||
const camerasContent = cameras.length > 0
|
const camerasContent = cameras.length > 0
|
||||||
? <HeaderList
|
? <HeaderList
|
||||||
key="cameras"
|
key="cameras"
|
||||||
title={APP_TEXT.category.cameraPlural}
|
title={appText.category.cameraPlural}
|
||||||
icon={<IconCamera
|
icon={<IconCamera
|
||||||
size={15}
|
size={15}
|
||||||
className="translate-x-[0.5px]"
|
className="translate-x-[0.5px]"
|
||||||
@ -107,7 +113,7 @@ export default function PhotoGridSidebar({
|
|||||||
const lensesContent = lenses.length > 0
|
const lensesContent = lenses.length > 0
|
||||||
? <HeaderList
|
? <HeaderList
|
||||||
key="lenses"
|
key="lenses"
|
||||||
title={APP_TEXT.category.lensPlural}
|
title={appText.category.lensPlural}
|
||||||
icon={<IconLens size={15} />}
|
icon={<IconLens size={15} />}
|
||||||
maxItems={maxItemsPerCategory}
|
maxItems={maxItemsPerCategory}
|
||||||
items={lenses
|
items={lenses
|
||||||
@ -127,7 +133,7 @@ export default function PhotoGridSidebar({
|
|||||||
const tagsContent = tags.length > 0
|
const tagsContent = tags.length > 0
|
||||||
? <HeaderList
|
? <HeaderList
|
||||||
key="tags"
|
key="tags"
|
||||||
title={APP_TEXT.category.tagPlural}
|
title={appText.category.tagPlural}
|
||||||
icon={<IconTag
|
icon={<IconTag
|
||||||
size={14}
|
size={14}
|
||||||
className="translate-x-[1px] translate-y-[1px]"
|
className="translate-x-[1px] translate-y-[1px]"
|
||||||
@ -172,7 +178,7 @@ export default function PhotoGridSidebar({
|
|||||||
const recipesContent = recipes.length > 0
|
const recipesContent = recipes.length > 0
|
||||||
? <HeaderList
|
? <HeaderList
|
||||||
key="recipes"
|
key="recipes"
|
||||||
title={APP_TEXT.category.recipePlural}
|
title={appText.category.recipePlural}
|
||||||
icon={<IconRecipe
|
icon={<IconRecipe
|
||||||
size={16}
|
size={16}
|
||||||
className="translate-x-[-1px]"
|
className="translate-x-[-1px]"
|
||||||
@ -195,7 +201,7 @@ export default function PhotoGridSidebar({
|
|||||||
const filmsContent = films.length > 0
|
const filmsContent = films.length > 0
|
||||||
? <HeaderList
|
? <HeaderList
|
||||||
key="films"
|
key="films"
|
||||||
title={APP_TEXT.category.filmPlural}
|
title={appText.category.filmPlural}
|
||||||
icon={<IconFilm size={15} />}
|
icon={<IconFilm size={15} />}
|
||||||
maxItems={maxItemsPerCategory}
|
maxItems={maxItemsPerCategory}
|
||||||
items={films
|
items={films
|
||||||
@ -213,7 +219,7 @@ export default function PhotoGridSidebar({
|
|||||||
const focalLengthsContent = focalLengths.length > 0
|
const focalLengthsContent = focalLengths.length > 0
|
||||||
? <HeaderList
|
? <HeaderList
|
||||||
key="focal-lengths"
|
key="focal-lengths"
|
||||||
title={APP_TEXT.category.focalLengthPlural}
|
title={appText.category.focalLengthPlural}
|
||||||
icon={<IconFocalLength size={13} />}
|
icon={<IconFocalLength size={13} />}
|
||||||
maxItems={maxItemsPerCategory}
|
maxItems={maxItemsPerCategory}
|
||||||
items={focalLengths.map(({ focal, count }) =>
|
items={focalLengths.map(({ focal, count }) =>
|
||||||
@ -232,14 +238,14 @@ export default function PhotoGridSidebar({
|
|||||||
? start
|
? start
|
||||||
? <HeaderList
|
? <HeaderList
|
||||||
key="photo-stats"
|
key="photo-stats"
|
||||||
title={photoQuantityText(photosCount, false)}
|
title={photoQuantityText(photosCount, appText, false)}
|
||||||
items={start === end
|
items={start === end
|
||||||
? [start]
|
? [start]
|
||||||
: [`${end} –`, start]}
|
: [`${end} –`, start]}
|
||||||
/>
|
/>
|
||||||
: <HeaderList
|
: <HeaderList
|
||||||
key="photo-stats"
|
key="photo-stats"
|
||||||
items={[photoQuantityText(photosCount, false)]}
|
items={[photoQuantityText(photosCount, appText, false)]}
|
||||||
/>
|
/>
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
|||||||
@ -17,13 +17,13 @@ import PhotoLink from './PhotoLink';
|
|||||||
import ResponsiveText from '@/components/primitives/ResponsiveText';
|
import ResponsiveText from '@/components/primitives/ResponsiveText';
|
||||||
import { useAppState } from '@/state/AppState';
|
import { useAppState } from '@/state/AppState';
|
||||||
import { GRID_GAP_CLASSNAME } from '@/components';
|
import { GRID_GAP_CLASSNAME } from '@/components';
|
||||||
import { APP_TEXT } from '@/app/config';
|
import { useAppText } from '@/i18n/state/client';
|
||||||
|
|
||||||
export default function PhotoHeader({
|
export default function PhotoHeader({
|
||||||
photos,
|
photos,
|
||||||
selectedPhoto,
|
selectedPhoto,
|
||||||
entity,
|
entity,
|
||||||
entityVerb = APP_TEXT.photo.photo.toLocaleUpperCase(),
|
entityVerb: _entityVerb,
|
||||||
entityDescription,
|
entityDescription,
|
||||||
indexNumber,
|
indexNumber,
|
||||||
count,
|
count,
|
||||||
@ -45,6 +45,10 @@ export default function PhotoHeader({
|
|||||||
} & PhotoSetCategory) {
|
} & PhotoSetCategory) {
|
||||||
const { isGridHighDensity } = useAppState();
|
const { isGridHighDensity } = useAppState();
|
||||||
|
|
||||||
|
const appText = useAppText();
|
||||||
|
|
||||||
|
const entityVerb = _entityVerb ?? appText.photo.photo.toLocaleUpperCase();
|
||||||
|
|
||||||
const { start, end } = dateRangeForPhotos(photos, dateRange);
|
const { start, end } = dateRangeForPhotos(photos, dateRange);
|
||||||
|
|
||||||
const selectedPhotoIndex = selectedPhoto
|
const selectedPhotoIndex = selectedPhoto
|
||||||
@ -155,13 +159,13 @@ export default function PhotoHeader({
|
|||||||
}} />}
|
}} />}
|
||||||
</>
|
</>
|
||||||
: <ResponsiveText
|
: <ResponsiveText
|
||||||
shortText={APP_TEXT.utility.paginateAction(
|
shortText={appText.utility.paginateAction(
|
||||||
paginationIndex,
|
paginationIndex,
|
||||||
paginationCount,
|
paginationCount,
|
||||||
entityVerb,
|
entityVerb,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{APP_TEXT.utility.paginateAction(
|
{appText.utility.paginateAction(
|
||||||
paginationIndex,
|
paginationIndex,
|
||||||
paginationCount,
|
paginationCount,
|
||||||
entityVerb)}
|
entityVerb)}
|
||||||
|
|||||||
@ -31,7 +31,6 @@ import {
|
|||||||
SHOW_TAKEN_AT_TIME,
|
SHOW_TAKEN_AT_TIME,
|
||||||
MATTE_COLOR,
|
MATTE_COLOR,
|
||||||
MATTE_COLOR_DARK,
|
MATTE_COLOR_DARK,
|
||||||
APP_TEXT,
|
|
||||||
} from '@/app/config';
|
} from '@/app/config';
|
||||||
import AdminPhotoMenu from '@/admin/AdminPhotoMenu';
|
import AdminPhotoMenu from '@/admin/AdminPhotoMenu';
|
||||||
import { RevalidatePhoto } from './InfinitePhotoScroll';
|
import { RevalidatePhoto } from './InfinitePhotoScroll';
|
||||||
@ -51,6 +50,7 @@ import PhotoLens from '@/lens/PhotoLens';
|
|||||||
import { lensFromPhoto } from '@/lens';
|
import { lensFromPhoto } from '@/lens';
|
||||||
import MaskedScroll from '@/components/MaskedScroll';
|
import MaskedScroll from '@/components/MaskedScroll';
|
||||||
import useCategoryCountsForPhoto from '@/category/useCategoryCountsForPhoto';
|
import useCategoryCountsForPhoto from '@/category/useCategoryCountsForPhoto';
|
||||||
|
import { useAppText } from '@/i18n/state/client';
|
||||||
|
|
||||||
export default function PhotoLarge({
|
export default function PhotoLarge({
|
||||||
photo,
|
photo,
|
||||||
@ -117,6 +117,8 @@ export default function PhotoLarge({
|
|||||||
isUserSignedIn,
|
isUserSignedIn,
|
||||||
} = useAppState();
|
} = useAppState();
|
||||||
|
|
||||||
|
const appText = useAppText();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
cameraCount,
|
cameraCount,
|
||||||
lensCount,
|
lensCount,
|
||||||
@ -379,7 +381,7 @@ export default function PhotoLarge({
|
|||||||
<>
|
<>
|
||||||
{' '}
|
{' '}
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={APP_TEXT.tooltip['35mm']}
|
content={appText.tooltip['35mm']}
|
||||||
sideOffset={3}
|
sideOffset={3}
|
||||||
supportMobile
|
supportMobile
|
||||||
>
|
>
|
||||||
@ -435,7 +437,7 @@ export default function PhotoLarge({
|
|||||||
)}>
|
)}>
|
||||||
{showZoomControls &&
|
{showZoomControls &&
|
||||||
<LoaderButton
|
<LoaderButton
|
||||||
tooltip={APP_TEXT.tooltip.zoom}
|
tooltip={appText.tooltip.zoom}
|
||||||
icon={<LuExpand size={15} />}
|
icon={<LuExpand size={15} />}
|
||||||
onClick={() => refZoomControls.current?.open()}
|
onClick={() => refZoomControls.current?.open()}
|
||||||
styleAs="link"
|
styleAs="link"
|
||||||
@ -444,7 +446,7 @@ export default function PhotoLarge({
|
|||||||
/>}
|
/>}
|
||||||
{shouldShare &&
|
{shouldShare &&
|
||||||
<ShareButton
|
<ShareButton
|
||||||
tooltip={APP_TEXT.tooltip.sharePhoto}
|
tooltip={appText.tooltip.sharePhoto}
|
||||||
photo={photo}
|
photo={photo}
|
||||||
tag={shouldShareTag
|
tag={shouldShareTag
|
||||||
? primaryTag
|
? primaryTag
|
||||||
|
|||||||
@ -25,13 +25,13 @@ import { isPhotoFav } from '@/tag';
|
|||||||
import Tooltip from '@/components/Tooltip';
|
import Tooltip from '@/components/Tooltip';
|
||||||
import {
|
import {
|
||||||
ALLOW_PUBLIC_DOWNLOADS,
|
ALLOW_PUBLIC_DOWNLOADS,
|
||||||
APP_TEXT,
|
|
||||||
SHOW_KEYBOARD_SHORTCUT_TOOLTIPS,
|
SHOW_KEYBOARD_SHORTCUT_TOOLTIPS,
|
||||||
} from '@/app/config';
|
} from '@/app/config';
|
||||||
import { downloadFileFromBrowser } from '@/utility/url';
|
import { downloadFileFromBrowser } from '@/utility/url';
|
||||||
import useKeydownHandler from '@/utility/useKeydownHandler';
|
import useKeydownHandler from '@/utility/useKeydownHandler';
|
||||||
import { KEY_COMMANDS } from './key-commands';
|
import { KEY_COMMANDS } from './key-commands';
|
||||||
import { syncPhotoConfirmText } from '@/admin/confirm';
|
import { syncPhotoConfirmText } from '@/admin/confirm';
|
||||||
|
import { useAppText } from '@/i18n/state/client';
|
||||||
|
|
||||||
const ANIMATION_LEFT: AnimationConfig = { type: 'left', duration: 0.3 };
|
const ANIMATION_LEFT: AnimationConfig = { type: 'left', duration: 0.3 };
|
||||||
const ANIMATION_RIGHT: AnimationConfig = { type: 'right', duration: 0.3 };
|
const ANIMATION_RIGHT: AnimationConfig = { type: 'right', duration: 0.3 };
|
||||||
@ -50,6 +50,8 @@ export default function PhotoPrevNextActions({
|
|||||||
} & PhotoSetCategory) {
|
} & PhotoSetCategory) {
|
||||||
const { setNextPhotoAnimation, isUserSignedIn } = useAppState();
|
const { setNextPhotoAnimation, isUserSignedIn } = useAppState();
|
||||||
|
|
||||||
|
const appText = useAppText();
|
||||||
|
|
||||||
const photoTitle = photo
|
const photoTitle = photo
|
||||||
? photo.title
|
? photo.title
|
||||||
? `'${photo.title}'`
|
? `'${photo.title}'`
|
||||||
@ -201,7 +203,7 @@ export default function PhotoPrevNextActions({
|
|||||||
'*:select-none',
|
'*:select-none',
|
||||||
)}>
|
)}>
|
||||||
<Tooltip {...SHOW_KEYBOARD_SHORTCUT_TOOLTIPS && {
|
<Tooltip {...SHOW_KEYBOARD_SHORTCUT_TOOLTIPS && {
|
||||||
content: APP_TEXT.nav.prev,
|
content: appText.nav.prev,
|
||||||
keyCommand: KEY_COMMANDS.prev[0],
|
keyCommand: KEY_COMMANDS.prev[0],
|
||||||
}}>
|
}}>
|
||||||
<PhotoLink
|
<PhotoLink
|
||||||
@ -215,7 +217,7 @@ export default function PhotoPrevNextActions({
|
|||||||
>
|
>
|
||||||
<FiChevronLeft className="sm:hidden text-[1.1rem]" />
|
<FiChevronLeft className="sm:hidden text-[1.1rem]" />
|
||||||
<span className="hidden sm:inline-block uppercase">
|
<span className="hidden sm:inline-block uppercase">
|
||||||
{APP_TEXT.nav.prevShort}
|
{appText.nav.prevShort}
|
||||||
</span>
|
</span>
|
||||||
</PhotoLink>
|
</PhotoLink>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@ -223,7 +225,7 @@ export default function PhotoPrevNextActions({
|
|||||||
/
|
/
|
||||||
</span>
|
</span>
|
||||||
<Tooltip {...SHOW_KEYBOARD_SHORTCUT_TOOLTIPS && {
|
<Tooltip {...SHOW_KEYBOARD_SHORTCUT_TOOLTIPS && {
|
||||||
content: APP_TEXT.nav.next,
|
content: appText.nav.next,
|
||||||
keyCommand: KEY_COMMANDS.next[0],
|
keyCommand: KEY_COMMANDS.next[0],
|
||||||
}}>
|
}}>
|
||||||
<PhotoLink
|
<PhotoLink
|
||||||
@ -237,7 +239,7 @@ export default function PhotoPrevNextActions({
|
|||||||
>
|
>
|
||||||
<FiChevronRight className="sm:hidden text-[1.1rem]" />
|
<FiChevronRight className="sm:hidden text-[1.1rem]" />
|
||||||
<span className="hidden sm:inline-block uppercase">
|
<span className="hidden sm:inline-block uppercase">
|
||||||
{APP_TEXT.nav.nextShort}
|
{appText.nav.nextShort}
|
||||||
</span>
|
</span>
|
||||||
</PhotoLink>
|
</PhotoLink>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import { useRef } from 'react';
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import Spinner from '@/components/Spinner';
|
import Spinner from '@/components/Spinner';
|
||||||
import ResponsiveText from '@/components/primitives/ResponsiveText';
|
import ResponsiveText from '@/components/primitives/ResponsiveText';
|
||||||
import { APP_TEXT } from '@/app/config';
|
import { useAppText } from '@/i18n/state/client';
|
||||||
|
|
||||||
export default function PhotoUploadWithStatus({
|
export default function PhotoUploadWithStatus({
|
||||||
inputRef,
|
inputRef,
|
||||||
@ -45,6 +45,8 @@ export default function PhotoUploadWithStatus({
|
|||||||
resetUploadState,
|
resetUploadState,
|
||||||
} = useAppState();
|
} = useAppState();
|
||||||
|
|
||||||
|
const appText = useAppText();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -77,7 +79,7 @@ export default function PhotoUploadWithStatus({
|
|||||||
const isFinishing = isPending && shouldResetUploadStateAfterPending.current;
|
const isFinishing = isPending && shouldResetUploadStateAfterPending.current;
|
||||||
|
|
||||||
const uploadStatusText = filesLength > 1
|
const uploadStatusText = filesLength > 1
|
||||||
? APP_TEXT.utility.paginate(fileUploadIndex + 1, filesLength)
|
? appText.utility.paginate(fileUploadIndex + 1, filesLength)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -160,19 +162,19 @@ export default function PhotoUploadWithStatus({
|
|||||||
{isUploading
|
{isUploading
|
||||||
? isFinishing
|
? isFinishing
|
||||||
? <>
|
? <>
|
||||||
{APP_TEXT.misc.finishing}
|
{appText.misc.finishing}
|
||||||
</>
|
</>
|
||||||
: <>
|
: <>
|
||||||
{!showButton && uploadStatusText
|
{!showButton && uploadStatusText
|
||||||
? <>
|
? <>
|
||||||
<ResponsiveText shortText={uploadStatusText}>
|
<ResponsiveText shortText={uploadStatusText}>
|
||||||
{APP_TEXT.misc.uploading} {uploadStatusText}
|
{appText.misc.uploading} {uploadStatusText}
|
||||||
</ResponsiveText>
|
</ResponsiveText>
|
||||||
{': '}
|
{': '}
|
||||||
{fileUploadName}
|
{fileUploadName}
|
||||||
</>
|
</>
|
||||||
: <ResponsiveText shortText={fileUploadName}>
|
: <ResponsiveText shortText={fileUploadName}>
|
||||||
{APP_TEXT.misc.uploading} {fileUploadName}
|
{appText.misc.uploading} {fileUploadName}
|
||||||
</ResponsiveText>}
|
</ResponsiveText>}
|
||||||
</>
|
</>
|
||||||
: !showButton && <>Initializing</>}
|
: !showButton && <>Initializing</>}
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import Container from '@/components/Container';
|
import Container from '@/components/Container';
|
||||||
import AppGrid from '@/components/AppGrid';
|
import AppGrid from '@/components/AppGrid';
|
||||||
import {
|
import {
|
||||||
APP_TEXT,
|
|
||||||
IS_SITE_READY,
|
IS_SITE_READY,
|
||||||
PRESERVE_ORIGINAL_UPLOADS,
|
PRESERVE_ORIGINAL_UPLOADS,
|
||||||
} from '@/app/config';
|
} from '@/app/config';
|
||||||
@ -13,8 +12,11 @@ import SignInOrUploadClient from '@/admin/SignInOrUploadClient';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { PATH_ADMIN_CONFIGURATION } from '@/app/paths';
|
import { PATH_ADMIN_CONFIGURATION } from '@/app/paths';
|
||||||
import AnimateItems from '@/components/AnimateItems';
|
import AnimateItems from '@/components/AnimateItems';
|
||||||
|
import { getAppText } from '@/i18n/state/server';
|
||||||
|
|
||||||
|
export default async function PhotosEmptyState() {
|
||||||
|
const appText = await getAppText();
|
||||||
|
|
||||||
export default function PhotosEmptyState() {
|
|
||||||
return (
|
return (
|
||||||
<AppGrid
|
<AppGrid
|
||||||
contentMain={
|
contentMain={
|
||||||
@ -34,8 +36,8 @@ export default function PhotosEmptyState() {
|
|||||||
'text-gray-700 dark:text-gray-200',
|
'text-gray-700 dark:text-gray-200',
|
||||||
)}>
|
)}>
|
||||||
{!IS_SITE_READY
|
{!IS_SITE_READY
|
||||||
? APP_TEXT.onboarding.setupIncomplete
|
? appText.onboarding.setupIncomplete
|
||||||
: APP_TEXT.onboarding.setupComplete}
|
: appText.onboarding.setupComplete}
|
||||||
</div>
|
</div>
|
||||||
{!IS_SITE_READY
|
{!IS_SITE_READY
|
||||||
? <AdminAppConfiguration simplifiedView />
|
? <AdminAppConfiguration simplifiedView />
|
||||||
@ -49,7 +51,7 @@ export default function PhotosEmptyState() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
{APP_TEXT.onboarding.setupConfig}
|
{appText.onboarding.setupConfig}
|
||||||
{' '}
|
{' '}
|
||||||
<Link
|
<Link
|
||||||
href={PATH_ADMIN_CONFIGURATION}
|
href={PATH_ADMIN_CONFIGURATION}
|
||||||
|
|||||||
@ -46,6 +46,7 @@ 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 FieldsetHidden from './FieldsetHidden';
|
||||||
|
import { useAppText } from '@/i18n/state/client';
|
||||||
|
|
||||||
const THUMBNAIL_SIZE = 300;
|
const THUMBNAIL_SIZE = 300;
|
||||||
|
|
||||||
@ -84,6 +85,8 @@ export default function PhotoForm({
|
|||||||
|
|
||||||
const { invalidateSwr, shouldDebugImageFallbacks } = useAppState();
|
const { invalidateSwr, shouldDebugImageFallbacks } = useAppState();
|
||||||
|
|
||||||
|
const appText = useAppText();
|
||||||
|
|
||||||
const changedFormKeys = useMemo(() =>
|
const changedFormKeys = useMemo(() =>
|
||||||
getChangedFormFields(initialPhotoForm, formData),
|
getChangedFormFields(initialPhotoForm, formData),
|
||||||
[initialPhotoForm, formData]);
|
[initialPhotoForm, formData]);
|
||||||
@ -328,7 +331,7 @@ export default function PhotoForm({
|
|||||||
{/* Fields */}
|
{/* Fields */}
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{FORM_METADATA_ENTRIES(
|
{FORM_METADATA_ENTRIES(
|
||||||
convertTagsForForm(uniqueTags),
|
convertTagsForForm(uniqueTags, appText),
|
||||||
convertRecipesForForm(uniqueRecipes),
|
convertRecipesForForm(uniqueRecipes),
|
||||||
convertFilmsForForm(uniqueFilms, isMakeFujifilm(formData.make)),
|
convertFilmsForForm(uniqueFilms, isMakeFujifilm(formData.make)),
|
||||||
aiContent !== undefined,
|
aiContent !== undefined,
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import { formatFocalLength } from '@/focal';
|
|||||||
import { getNextImageUrlForRequest } from '@/platforms/next-image';
|
import { getNextImageUrlForRequest } from '@/platforms/next-image';
|
||||||
import { photoHasFilmData } from '@/film';
|
import { photoHasFilmData } from '@/film';
|
||||||
import {
|
import {
|
||||||
APP_TEXT,
|
|
||||||
HIGH_DENSITY_GRID,
|
HIGH_DENSITY_GRID,
|
||||||
IS_PREVIEW,
|
IS_PREVIEW,
|
||||||
SHOW_EXIF_DATA,
|
SHOW_EXIF_DATA,
|
||||||
@ -25,6 +24,7 @@ import type { Metadata } from 'next';
|
|||||||
import { FujifilmRecipe } from '@/platforms/fujifilm/recipe';
|
import { FujifilmRecipe } from '@/platforms/fujifilm/recipe';
|
||||||
import { FujifilmSimulation } from '@/platforms/fujifilm/simulation';
|
import { FujifilmSimulation } from '@/platforms/fujifilm/simulation';
|
||||||
import { PhotoSyncStatus, generatePhotoSyncStatus } from './sync';
|
import { PhotoSyncStatus, generatePhotoSyncStatus } from './sync';
|
||||||
|
import { I18NState } from '@/i18n/state';
|
||||||
|
|
||||||
// INFINITE SCROLL: FEED
|
// INFINITE SCROLL: FEED
|
||||||
export const INFINITE_SCROLL_FEED_INITIAL =
|
export const INFINITE_SCROLL_FEED_INITIAL =
|
||||||
@ -232,10 +232,14 @@ export const titleForPhoto = (
|
|||||||
export const altTextForPhoto = (photo: Photo) =>
|
export const altTextForPhoto = (photo: Photo) =>
|
||||||
photo.semanticDescription || titleForPhoto(photo);
|
photo.semanticDescription || titleForPhoto(photo);
|
||||||
|
|
||||||
export const photoLabelForCount = (count: number, _capitalize = true) => {
|
export const photoLabelForCount = (
|
||||||
|
count: number,
|
||||||
|
appText: I18NState,
|
||||||
|
_capitalize = true,
|
||||||
|
) => {
|
||||||
const label = count === 1
|
const label = count === 1
|
||||||
? APP_TEXT.photo.photo
|
? appText.photo.photo
|
||||||
: APP_TEXT.photo.photoPlural;
|
: appText.photo.photoPlural;
|
||||||
return _capitalize
|
return _capitalize
|
||||||
? capitalize(label)
|
? capitalize(label)
|
||||||
: label;
|
: label;
|
||||||
@ -243,31 +247,38 @@ export const photoLabelForCount = (count: number, _capitalize = true) => {
|
|||||||
|
|
||||||
export const photoQuantityText = (
|
export const photoQuantityText = (
|
||||||
count: number,
|
count: number,
|
||||||
|
appText: I18NState,
|
||||||
includeParentheses = true,
|
includeParentheses = true,
|
||||||
capitalize?: boolean,
|
capitalize?: boolean,
|
||||||
) =>
|
) =>
|
||||||
includeParentheses
|
includeParentheses
|
||||||
? `(${count} ${photoLabelForCount(count, capitalize)})`
|
? `(${count} ${photoLabelForCount(count, appText, capitalize)})`
|
||||||
: `${count} ${photoLabelForCount(count, capitalize)}`;
|
: `${count} ${photoLabelForCount(count, appText, capitalize)}`;
|
||||||
|
|
||||||
export const deleteConfirmationTextForPhoto = (photo: Photo) =>
|
export const deleteConfirmationTextForPhoto = (
|
||||||
APP_TEXT.admin.deleteConfirm(titleForPhoto(photo));
|
photo: Photo,
|
||||||
|
appText: I18NState,
|
||||||
|
) =>
|
||||||
|
appText.admin.deleteConfirm(titleForPhoto(photo));
|
||||||
|
|
||||||
export type PhotoDateRange = { start: string, end: string };
|
export type PhotoDateRange = { start: string, end: string };
|
||||||
|
|
||||||
export const descriptionForPhotoSet = (
|
export const descriptionForPhotoSet = (
|
||||||
photos:Photo[] = [],
|
photos:Photo[] = [],
|
||||||
|
appText: I18NState,
|
||||||
descriptor?: string,
|
descriptor?: string,
|
||||||
dateBased?: boolean,
|
dateBased?: boolean,
|
||||||
explicitCount?: number,
|
explicitCount?: number,
|
||||||
explicitDateRange?: PhotoDateRange,
|
explicitDateRange?: PhotoDateRange,
|
||||||
) =>
|
) =>
|
||||||
dateBased
|
dateBased
|
||||||
? dateRangeForPhotos(photos, explicitDateRange).description.toUpperCase()
|
? dateRangeForPhotos(photos, explicitDateRange)
|
||||||
|
.description
|
||||||
|
.toLocaleUpperCase()
|
||||||
: [
|
: [
|
||||||
explicitCount ?? photos.length, (
|
explicitCount ?? photos.length, (
|
||||||
descriptor ||
|
descriptor ||
|
||||||
photoLabelForCount(explicitCount ?? photos.length, false)
|
photoLabelForCount(explicitCount ?? photos.length, appText, false)
|
||||||
),
|
),
|
||||||
].join(' ');
|
].join(' ');
|
||||||
|
|
||||||
|
|||||||
@ -19,7 +19,7 @@ import { TbChecklist } from 'react-icons/tb';
|
|||||||
import CopyButton from '@/components/CopyButton';
|
import CopyButton from '@/components/CopyButton';
|
||||||
import { labelForFilm } from '@/film';
|
import { labelForFilm } from '@/film';
|
||||||
import PhotoRecipe from './PhotoRecipe';
|
import PhotoRecipe from './PhotoRecipe';
|
||||||
import { APP_TEXT } from '@/app/config';
|
import { useAppText } from '@/i18n/state/client';
|
||||||
|
|
||||||
export default function PhotoRecipeOverlay({
|
export default function PhotoRecipeOverlay({
|
||||||
ref,
|
ref,
|
||||||
@ -47,6 +47,8 @@ export default function PhotoRecipeOverlay({
|
|||||||
bwMagentaGreen,
|
bwMagentaGreen,
|
||||||
} = data;
|
} = data;
|
||||||
|
|
||||||
|
const appText = useAppText();
|
||||||
|
|
||||||
const whiteBalanceTypeFormatted = formatWhiteBalance(data);
|
const whiteBalanceTypeFormatted = formatWhiteBalance(data);
|
||||||
|
|
||||||
const renderDataSquare = (
|
const renderDataSquare = (
|
||||||
@ -139,7 +141,7 @@ export default function PhotoRecipeOverlay({
|
|||||||
'text-black/40 active:text-black/75',
|
'text-black/40 active:text-black/75',
|
||||||
'hover:text-black/40',
|
'hover:text-black/40',
|
||||||
)}
|
)}
|
||||||
tooltip={APP_TEXT.tooltip.recipeCopy}
|
tooltip={appText.tooltip.recipeCopy}
|
||||||
tooltipColor="frosted"
|
tooltipColor="frosted"
|
||||||
/>
|
/>
|
||||||
<span>
|
<span>
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import clsx from 'clsx/lite';
|
|||||||
import { FaPlus } from 'react-icons/fa6';
|
import { FaPlus } from 'react-icons/fa6';
|
||||||
import Tooltip from '@/components/Tooltip';
|
import Tooltip from '@/components/Tooltip';
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import { APP_TEXT } from '@/app/config';
|
import { useAppText } from '@/i18n/state/client';
|
||||||
|
|
||||||
export default function PhotoRecipeOverlayButton({
|
export default function PhotoRecipeOverlayButton({
|
||||||
className,
|
className,
|
||||||
@ -17,8 +17,10 @@ export default function PhotoRecipeOverlayButton({
|
|||||||
}) {
|
}) {
|
||||||
const ref = useRef<HTMLButtonElement>(null);
|
const ref = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
|
const appText = useAppText();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip content={APP_TEXT.tooltip.recipeInfo}>
|
<Tooltip content={appText.tooltip.recipeInfo}>
|
||||||
<button
|
<button
|
||||||
ref={ref}
|
ref={ref}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import PhotoRecipe from './PhotoRecipe';
|
|||||||
import { useAppState } from '@/state/AppState';
|
import { useAppState } from '@/state/AppState';
|
||||||
import { descriptionForRecipePhotos, getRecipePropsFromPhotos } from '.';
|
import { descriptionForRecipePhotos, getRecipePropsFromPhotos } from '.';
|
||||||
import { AI_TEXT_GENERATION_ENABLED } from '@/app/config';
|
import { AI_TEXT_GENERATION_ENABLED } from '@/app/config';
|
||||||
|
import { useAppText } from '@/i18n/state/client';
|
||||||
|
|
||||||
export default function RecipeHeader({
|
export default function RecipeHeader({
|
||||||
recipe,
|
recipe,
|
||||||
@ -24,6 +25,8 @@ export default function RecipeHeader({
|
|||||||
}) {
|
}) {
|
||||||
const { recipeModalProps, setRecipeModalProps } = useAppState();
|
const { recipeModalProps, setRecipeModalProps } = useAppState();
|
||||||
|
|
||||||
|
const appText = useAppText();
|
||||||
|
|
||||||
const recipeProps = getRecipePropsFromPhotos(photos, selectedPhoto);
|
const recipeProps = getRecipePropsFromPhotos(photos, selectedPhoto);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -37,7 +40,13 @@ export default function RecipeHeader({
|
|||||||
? () => setRecipeModalProps?.(recipeProps)
|
? () => setRecipeModalProps?.(recipeProps)
|
||||||
: undefined}
|
: undefined}
|
||||||
/>}
|
/>}
|
||||||
entityDescription={descriptionForRecipePhotos(photos, undefined, count)}
|
entityDescription={descriptionForRecipePhotos(
|
||||||
|
photos,
|
||||||
|
appText,
|
||||||
|
undefined,
|
||||||
|
count,
|
||||||
|
dateRange,
|
||||||
|
)}
|
||||||
photos={photos}
|
photos={photos}
|
||||||
selectedPhoto={selectedPhoto}
|
selectedPhoto={selectedPhoto}
|
||||||
indexNumber={indexNumber}
|
indexNumber={indexNumber}
|
||||||
|
|||||||
@ -2,8 +2,9 @@ import { Photo, PhotoDateRange } from '@/photo';
|
|||||||
import { absolutePathForRecipeImage, pathForRecipe } from '@/app/paths';
|
import { absolutePathForRecipeImage, pathForRecipe } from '@/app/paths';
|
||||||
import OGTile, { OGLoadingState } from '@/components/OGTile';
|
import OGTile, { OGLoadingState } from '@/components/OGTile';
|
||||||
import { descriptionForRecipePhotos, titleForRecipe } from '.';
|
import { descriptionForRecipePhotos, titleForRecipe } from '.';
|
||||||
|
import { getAppText } from '@/i18n/state/server';
|
||||||
|
|
||||||
export default function RecipeOGTile({
|
export default async function RecipeOGTile({
|
||||||
recipe,
|
recipe,
|
||||||
photos,
|
photos,
|
||||||
loadingState: loadingStateExternal,
|
loadingState: loadingStateExternal,
|
||||||
@ -24,10 +25,17 @@ export default function RecipeOGTile({
|
|||||||
count?: number
|
count?: number
|
||||||
dateRange?: PhotoDateRange
|
dateRange?: PhotoDateRange
|
||||||
}) {
|
}) {
|
||||||
|
const appText = await getAppText();
|
||||||
return (
|
return (
|
||||||
<OGTile {...{
|
<OGTile {...{
|
||||||
title: titleForRecipe(recipe, photos, count),
|
title: titleForRecipe(recipe, photos, appText, count),
|
||||||
description: descriptionForRecipePhotos(photos, true, count, dateRange),
|
description: descriptionForRecipePhotos(
|
||||||
|
photos,
|
||||||
|
appText,
|
||||||
|
true,
|
||||||
|
count,
|
||||||
|
dateRange,
|
||||||
|
),
|
||||||
path: pathForRecipe(recipe),
|
path: pathForRecipe(recipe),
|
||||||
pathImageAbsolute: absolutePathForRecipeImage(recipe),
|
pathImageAbsolute: absolutePathForRecipeImage(recipe),
|
||||||
loadingState: loadingStateExternal,
|
loadingState: loadingStateExternal,
|
||||||
|
|||||||
@ -8,8 +8,9 @@ import {
|
|||||||
generateRecipeText,
|
generateRecipeText,
|
||||||
} from '.';
|
} from '.';
|
||||||
import RecipeOGTile from './RecipeOGTile';
|
import RecipeOGTile from './RecipeOGTile';
|
||||||
|
import { getAppText } from '@/i18n/state/server';
|
||||||
|
|
||||||
export default function RecipeShareModal({
|
export default async function RecipeShareModal({
|
||||||
recipe,
|
recipe,
|
||||||
photos,
|
photos,
|
||||||
count,
|
count,
|
||||||
@ -23,10 +24,12 @@ export default function RecipeShareModal({
|
|||||||
? generateRecipeText({ data, film })
|
? generateRecipeText({ data, film })
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
const appText = await getAppText();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ShareModal
|
<ShareModal
|
||||||
pathShare={absolutePathForRecipe(recipe, true)}
|
pathShare={absolutePathForRecipe(recipe, true)}
|
||||||
socialText={shareTextForRecipe(recipe)}
|
socialText={shareTextForRecipe(recipe, appText)}
|
||||||
navigatorTitle={formatRecipe(recipe)}
|
navigatorTitle={formatRecipe(recipe)}
|
||||||
navigatorText={recipeText}
|
navigatorText={recipeText}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import {
|
|||||||
} from '@/utility/string';
|
} from '@/utility/string';
|
||||||
import { FujifilmRecipe } from '@/platforms/fujifilm/recipe';
|
import { FujifilmRecipe } from '@/platforms/fujifilm/recipe';
|
||||||
import { labelForFilm } from '@/film';
|
import { labelForFilm } from '@/film';
|
||||||
import { APP_TEXT } from '@/app/config';
|
import { I18NState } from '@/i18n/state';
|
||||||
|
|
||||||
export type RecipeWithCount = {
|
export type RecipeWithCount = {
|
||||||
recipe: string
|
recipe: string
|
||||||
@ -31,23 +31,29 @@ export const formatRecipe = (recipe?: string) =>
|
|||||||
export const titleForRecipe = (
|
export const titleForRecipe = (
|
||||||
recipe: string,
|
recipe: string,
|
||||||
photos:Photo[] = [],
|
photos:Photo[] = [],
|
||||||
|
appText: I18NState,
|
||||||
explicitCount?: number,
|
explicitCount?: number,
|
||||||
) => [
|
) => [
|
||||||
`${APP_TEXT.category.recipe}: ${formatRecipe(recipe)}`,
|
`${appText.category.recipe}: ${formatRecipe(recipe)}`,
|
||||||
photoQuantityText(explicitCount ?? photos.length),
|
photoQuantityText(explicitCount ?? photos.length, appText),
|
||||||
].join(' ');
|
].join(' ');
|
||||||
|
|
||||||
export const shareTextForRecipe = (recipe: string) =>
|
export const shareTextForRecipe = (
|
||||||
APP_TEXT.category.recipeShare(formatRecipe(recipe));
|
recipe: string,
|
||||||
|
appText: I18NState,
|
||||||
|
) =>
|
||||||
|
appText.category.recipeShare(formatRecipe(recipe));
|
||||||
|
|
||||||
export const descriptionForRecipePhotos = (
|
export const descriptionForRecipePhotos = (
|
||||||
photos: Photo[] = [],
|
photos: Photo[] = [],
|
||||||
|
appText: I18NState,
|
||||||
dateBased?: boolean,
|
dateBased?: boolean,
|
||||||
explicitCount?: number,
|
explicitCount?: number,
|
||||||
explicitDateRange?: PhotoDateRange,
|
explicitDateRange?: PhotoDateRange,
|
||||||
) =>
|
) =>
|
||||||
descriptionForPhotoSet(
|
descriptionForPhotoSet(
|
||||||
photos,
|
photos,
|
||||||
|
appText,
|
||||||
undefined,
|
undefined,
|
||||||
dateBased,
|
dateBased,
|
||||||
explicitCount,
|
explicitCount,
|
||||||
@ -139,13 +145,20 @@ export const generateRecipeText = (
|
|||||||
export const generateMetaForRecipe = (
|
export const generateMetaForRecipe = (
|
||||||
recipe: string,
|
recipe: string,
|
||||||
photos: Photo[],
|
photos: Photo[],
|
||||||
|
appText: I18NState,
|
||||||
explicitCount?: number,
|
explicitCount?: number,
|
||||||
explicitDateRange?: PhotoDateRange,
|
explicitDateRange?: PhotoDateRange,
|
||||||
) => ({
|
) => ({
|
||||||
url: absolutePathForRecipe(recipe),
|
url: absolutePathForRecipe(recipe),
|
||||||
title: titleForRecipe(recipe, photos, explicitCount),
|
title: titleForRecipe(recipe, photos, appText, explicitCount),
|
||||||
description:
|
description:
|
||||||
descriptionForRecipePhotos(photos, true, explicitCount, explicitDateRange),
|
descriptionForRecipePhotos(
|
||||||
|
photos,
|
||||||
|
appText,
|
||||||
|
true,
|
||||||
|
explicitCount,
|
||||||
|
explicitDateRange,
|
||||||
|
),
|
||||||
images: absolutePathForRecipeImage(recipe),
|
images: absolutePathForRecipeImage(recipe),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -8,12 +8,13 @@ import { ReactNode, useEffect } from 'react';
|
|||||||
import { shortenUrl } from '@/utility/url';
|
import { shortenUrl } from '@/utility/url';
|
||||||
import { toastSuccess } from '@/toast';
|
import { toastSuccess } from '@/toast';
|
||||||
import { PiXLogo } from 'react-icons/pi';
|
import { PiXLogo } from 'react-icons/pi';
|
||||||
import { APP_TEXT, SHOW_SOCIAL } from '@/app/config';
|
import { SHOW_SOCIAL } from '@/app/config';
|
||||||
import { generateXPostText } from '@/utility/social';
|
import { generateXPostText } from '@/utility/social';
|
||||||
import { useAppState } from '@/state/AppState';
|
import { useAppState } from '@/state/AppState';
|
||||||
import useOnPathChange from '@/utility/useOnPathChange';
|
import useOnPathChange from '@/utility/useOnPathChange';
|
||||||
import { IoArrowUp } from 'react-icons/io5';
|
import { IoArrowUp } from 'react-icons/io5';
|
||||||
import MaskedScroll from '@/components/MaskedScroll';
|
import MaskedScroll from '@/components/MaskedScroll';
|
||||||
|
import { useAppText } from '@/i18n/state/client';
|
||||||
|
|
||||||
export default function ShareModal({
|
export default function ShareModal({
|
||||||
title,
|
title,
|
||||||
@ -35,6 +36,8 @@ export default function ShareModal({
|
|||||||
setShouldRespondToKeyboardCommands,
|
setShouldRespondToKeyboardCommands,
|
||||||
} = useAppState();
|
} = useAppState();
|
||||||
|
|
||||||
|
const appText = useAppText();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setShouldRespondToKeyboardCommands?.(false);
|
setShouldRespondToKeyboardCommands?.(false);
|
||||||
return () => setShouldRespondToKeyboardCommands?.(true);
|
return () => setShouldRespondToKeyboardCommands?.(true);
|
||||||
@ -96,7 +99,7 @@ export default function ShareModal({
|
|||||||
<BiCopy size={18} />,
|
<BiCopy size={18} />,
|
||||||
() => {
|
() => {
|
||||||
navigator.clipboard.writeText(pathShare);
|
navigator.clipboard.writeText(pathShare);
|
||||||
toastSuccess(APP_TEXT.photo.copied);
|
toastSuccess(appText.photo.copied);
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -2,8 +2,9 @@ import { Photo, photoQuantityText } from '@/photo';
|
|||||||
import PhotoHeader from '@/photo/PhotoHeader';
|
import PhotoHeader from '@/photo/PhotoHeader';
|
||||||
import HiddenTag from './HiddenTag';
|
import HiddenTag from './HiddenTag';
|
||||||
import { AI_TEXT_GENERATION_ENABLED } from '@/app/config';
|
import { AI_TEXT_GENERATION_ENABLED } from '@/app/config';
|
||||||
|
import { getAppText } from '@/i18n/state/server';
|
||||||
|
|
||||||
export default function HiddenHeader({
|
export default async function HiddenHeader({
|
||||||
photos,
|
photos,
|
||||||
selectedPhoto,
|
selectedPhoto,
|
||||||
indexNumber,
|
indexNumber,
|
||||||
@ -14,11 +15,12 @@ export default function HiddenHeader({
|
|||||||
indexNumber?: number
|
indexNumber?: number
|
||||||
count: number
|
count: number
|
||||||
}) {
|
}) {
|
||||||
|
const appText = await getAppText();
|
||||||
return (
|
return (
|
||||||
<PhotoHeader
|
<PhotoHeader
|
||||||
key="HiddenHeader"
|
key="HiddenHeader"
|
||||||
entity={<HiddenTag contrast="high" />}
|
entity={<HiddenTag contrast="high" />}
|
||||||
entityDescription={photoQuantityText(count, false)}
|
entityDescription={photoQuantityText(count, appText, false, false)}
|
||||||
photos={photos}
|
photos={photos}
|
||||||
selectedPhoto={selectedPhoto}
|
selectedPhoto={selectedPhoto}
|
||||||
indexNumber={indexNumber}
|
indexNumber={indexNumber}
|
||||||
|
|||||||
@ -3,9 +3,10 @@ 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 FavsTag from './FavsTag';
|
||||||
import { AI_TEXT_GENERATION_ENABLED, APP_TEXT } from '@/app/config';
|
import { AI_TEXT_GENERATION_ENABLED } from '@/app/config';
|
||||||
|
import { getAppText } from '@/i18n/state/server';
|
||||||
|
|
||||||
export default function TagHeader({
|
export default async function TagHeader({
|
||||||
tag,
|
tag,
|
||||||
photos,
|
photos,
|
||||||
selectedPhoto,
|
selectedPhoto,
|
||||||
@ -20,14 +21,20 @@ export default function TagHeader({
|
|||||||
count?: number
|
count?: number
|
||||||
dateRange?: PhotoDateRange
|
dateRange?: PhotoDateRange
|
||||||
}) {
|
}) {
|
||||||
|
const appText = await getAppText();
|
||||||
return (
|
return (
|
||||||
<PhotoHeader
|
<PhotoHeader
|
||||||
tag={tag}
|
tag={tag}
|
||||||
entity={isTagFavs(tag)
|
entity={isTagFavs(tag)
|
||||||
? <FavsTag contrast="high" />
|
? <FavsTag contrast="high" />
|
||||||
: <PhotoTag tag={tag} contrast="high" />}
|
: <PhotoTag tag={tag} contrast="high" />}
|
||||||
entityVerb={APP_TEXT.category.taggedPhotos}
|
entityVerb={appText.category.taggedPhotos}
|
||||||
entityDescription={descriptionForTaggedPhotos(photos, undefined, count)}
|
entityDescription={descriptionForTaggedPhotos(
|
||||||
|
photos,
|
||||||
|
appText,
|
||||||
|
undefined,
|
||||||
|
count,
|
||||||
|
)}
|
||||||
photos={photos}
|
photos={photos}
|
||||||
selectedPhoto={selectedPhoto}
|
selectedPhoto={selectedPhoto}
|
||||||
indexNumber={indexNumber}
|
indexNumber={indexNumber}
|
||||||
|
|||||||
@ -2,8 +2,9 @@ import { Photo, PhotoDateRange } from '@/photo';
|
|||||||
import { absolutePathForTagImage, pathForTag } from '@/app/paths';
|
import { absolutePathForTagImage, pathForTag } from '@/app/paths';
|
||||||
import OGTile, { OGLoadingState } from '@/components/OGTile';
|
import OGTile, { OGLoadingState } from '@/components/OGTile';
|
||||||
import { descriptionForTaggedPhotos, titleForTag } from '.';
|
import { descriptionForTaggedPhotos, titleForTag } from '.';
|
||||||
|
import { getAppText } from '@/i18n/state/server';
|
||||||
|
|
||||||
export default function TagOGTile({
|
export default async function TagOGTile({
|
||||||
tag,
|
tag,
|
||||||
photos,
|
photos,
|
||||||
loadingState: loadingStateExternal,
|
loadingState: loadingStateExternal,
|
||||||
@ -24,10 +25,17 @@ export default function TagOGTile({
|
|||||||
count?: number
|
count?: number
|
||||||
dateRange?: PhotoDateRange
|
dateRange?: PhotoDateRange
|
||||||
}) {
|
}) {
|
||||||
|
const appText = await getAppText();
|
||||||
return (
|
return (
|
||||||
<OGTile {...{
|
<OGTile {...{
|
||||||
title: titleForTag(tag, photos, count),
|
title: titleForTag(tag, photos, appText, count),
|
||||||
description: descriptionForTaggedPhotos(photos, true, count, dateRange),
|
description: descriptionForTaggedPhotos(
|
||||||
|
photos,
|
||||||
|
appText,
|
||||||
|
true,
|
||||||
|
count,
|
||||||
|
dateRange,
|
||||||
|
),
|
||||||
path: pathForTag(tag),
|
path: pathForTag(tag),
|
||||||
pathImageAbsolute: absolutePathForTagImage(tag),
|
pathImageAbsolute: absolutePathForTagImage(tag),
|
||||||
loadingState: loadingStateExternal,
|
loadingState: loadingStateExternal,
|
||||||
|
|||||||
@ -3,8 +3,9 @@ import { PhotoSetAttributes } from '../category';
|
|||||||
import ShareModal from '@/share/ShareModal';
|
import ShareModal from '@/share/ShareModal';
|
||||||
import TagOGTile from './TagOGTile';
|
import TagOGTile from './TagOGTile';
|
||||||
import { formatTag, shareTextForTag } from '.';
|
import { formatTag, shareTextForTag } from '.';
|
||||||
|
import { getAppText } from '@/i18n/state/server';
|
||||||
|
|
||||||
export default function TagShareModal({
|
export default async function TagShareModal({
|
||||||
tag,
|
tag,
|
||||||
photos,
|
photos,
|
||||||
count,
|
count,
|
||||||
@ -12,11 +13,12 @@ export default function TagShareModal({
|
|||||||
}: {
|
}: {
|
||||||
tag: string
|
tag: string
|
||||||
} & PhotoSetAttributes) {
|
} & PhotoSetAttributes) {
|
||||||
|
const appText = await getAppText();
|
||||||
return (
|
return (
|
||||||
<ShareModal
|
<ShareModal
|
||||||
pathShare={absolutePathForTag(tag, true)}
|
pathShare={absolutePathForTag(tag, true)}
|
||||||
navigatorTitle={formatTag(tag)}
|
navigatorTitle={formatTag(tag)}
|
||||||
socialText={shareTextForTag(tag)}
|
socialText={shareTextForTag(tag, appText)}
|
||||||
>
|
>
|
||||||
<TagOGTile {...{ tag, photos, count, dateRange }} />
|
<TagOGTile {...{ tag, photos, count, dateRange }} />
|
||||||
</ShareModal>
|
</ShareModal>
|
||||||
|
|||||||
@ -16,7 +16,7 @@ import {
|
|||||||
formatCountDescriptive,
|
formatCountDescriptive,
|
||||||
} from '@/utility/string';
|
} from '@/utility/string';
|
||||||
import { sortCategoryByCount } from '@/category';
|
import { sortCategoryByCount } from '@/category';
|
||||||
import { APP_TEXT } from '@/app/config';
|
import { I18NState } from '@/i18n/state';
|
||||||
|
|
||||||
// Reserved tags
|
// Reserved tags
|
||||||
export const TAG_FAVS = 'favs';
|
export const TAG_FAVS = 'favs';
|
||||||
@ -42,16 +42,20 @@ export const getValidationMessageForTags = (tags?: string) => {
|
|||||||
export const titleForTag = (
|
export const titleForTag = (
|
||||||
tag: string,
|
tag: string,
|
||||||
photos:Photo[] = [],
|
photos:Photo[] = [],
|
||||||
|
appText: I18NState,
|
||||||
explicitCount?: number,
|
explicitCount?: number,
|
||||||
) => [
|
) => [
|
||||||
formatTag(tag),
|
formatTag(tag),
|
||||||
photoQuantityText(explicitCount ?? photos.length),
|
photoQuantityText(explicitCount ?? photos.length, appText),
|
||||||
].join(' ');
|
].join(' ');
|
||||||
|
|
||||||
export const shareTextForTag = (tag: string) =>
|
export const shareTextForTag = (
|
||||||
|
tag: string,
|
||||||
|
appText: I18NState,
|
||||||
|
) =>
|
||||||
isTagFavs(tag)
|
isTagFavs(tag)
|
||||||
? APP_TEXT.category.taggedFavs
|
? appText.category.taggedFavs
|
||||||
: APP_TEXT.category.taggedPhrase(formatTag(tag));
|
: appText.category.taggedPhrase(formatTag(tag));
|
||||||
|
|
||||||
export const sortTagsArray = (
|
export const sortTagsArray = (
|
||||||
tags: string[],
|
tags: string[],
|
||||||
@ -92,13 +96,15 @@ export const sortTagsObjectWithoutFavs = (tags: Tags) =>
|
|||||||
|
|
||||||
export const descriptionForTaggedPhotos = (
|
export const descriptionForTaggedPhotos = (
|
||||||
photos: Photo[] = [],
|
photos: Photo[] = [],
|
||||||
|
appText: I18NState,
|
||||||
dateBased?: boolean,
|
dateBased?: boolean,
|
||||||
explicitCount?: number,
|
explicitCount?: number,
|
||||||
explicitDateRange?: PhotoDateRange,
|
explicitDateRange?: PhotoDateRange,
|
||||||
) =>
|
) =>
|
||||||
descriptionForPhotoSet(
|
descriptionForPhotoSet(
|
||||||
photos,
|
photos,
|
||||||
APP_TEXT.category.taggedPhotos,
|
appText,
|
||||||
|
appText.category.taggedPhotos,
|
||||||
dateBased,
|
dateBased,
|
||||||
explicitCount,
|
explicitCount,
|
||||||
explicitDateRange,
|
explicitDateRange,
|
||||||
@ -107,13 +113,19 @@ export const descriptionForTaggedPhotos = (
|
|||||||
export const generateMetaForTag = (
|
export const generateMetaForTag = (
|
||||||
tag: string,
|
tag: string,
|
||||||
photos: Photo[],
|
photos: Photo[],
|
||||||
|
appText: I18NState,
|
||||||
explicitCount?: number,
|
explicitCount?: number,
|
||||||
explicitDateRange?: PhotoDateRange,
|
explicitDateRange?: PhotoDateRange,
|
||||||
) => ({
|
) => ({
|
||||||
url: absolutePathForTag(tag),
|
url: absolutePathForTag(tag),
|
||||||
title: titleForTag(tag, photos, explicitCount),
|
title: titleForTag(tag, photos, appText, explicitCount),
|
||||||
description:
|
description: descriptionForTaggedPhotos(
|
||||||
descriptionForTaggedPhotos(photos, true, explicitCount, explicitDateRange),
|
photos,
|
||||||
|
appText,
|
||||||
|
true,
|
||||||
|
explicitCount,
|
||||||
|
explicitDateRange,
|
||||||
|
),
|
||||||
images: absolutePathForTagImage(tag),
|
images: absolutePathForTagImage(tag),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -137,11 +149,14 @@ export const addHiddenToTags = (tags: Tags, photosCountHidden = 0) =>
|
|||||||
)
|
)
|
||||||
: tags;
|
: tags;
|
||||||
|
|
||||||
export const convertTagsForForm = (tags: Tags = []) =>
|
export const convertTagsForForm = (
|
||||||
|
tags: Tags = [],
|
||||||
|
appText: I18NState,
|
||||||
|
) =>
|
||||||
sortTagsObjectWithoutFavs(tags)
|
sortTagsObjectWithoutFavs(tags)
|
||||||
.map(({ tag, count }) => ({
|
.map(({ tag, count }) => ({
|
||||||
value: tag,
|
value: tag,
|
||||||
annotation: formatCount(count),
|
annotation: formatCount(count),
|
||||||
annotationAria:
|
annotationAria:
|
||||||
formatCountDescriptive(count, APP_TEXT.category.taggedPhotos),
|
formatCountDescriptive(count, appText.category.taggedPhotos),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { parseISO, parse, format } from 'date-fns';
|
import { parseISO, parse, format } from 'date-fns';
|
||||||
import { formatInTimeZone } from 'date-fns-tz';
|
import { formatInTimeZone } from 'date-fns-tz';
|
||||||
import { Timezone } from './timezone';
|
import { Timezone } from './timezone';
|
||||||
import { APP_TEXT } from '@/app/config';
|
import { DATE_FN_LOCALE } from '@/i18n/date';
|
||||||
|
|
||||||
const DATE_STRING_FORMAT_TINY = 'dd MMM yy';
|
const DATE_STRING_FORMAT_TINY = 'dd MMM yy';
|
||||||
const DATE_STRING_FORMAT_TINY_PLACEHOLDER = '00 000 00';
|
const DATE_STRING_FORMAT_TINY_PLACEHOLDER = '00 000 00';
|
||||||
@ -67,12 +67,15 @@ export const formatDate = ({
|
|||||||
? placeholderString
|
? placeholderString
|
||||||
: timezone
|
: timezone
|
||||||
? formatInTimeZone(
|
? formatInTimeZone(
|
||||||
date, timezone, formatString, { locale: APP_TEXT.dateLocale },
|
date, timezone, formatString, { locale: DATE_FN_LOCALE },
|
||||||
)
|
)
|
||||||
: format(date, formatString, { locale: APP_TEXT.dateLocale });
|
: format(date, formatString, { locale: DATE_FN_LOCALE });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const formatDateFromPostgresString = (date: string, length?: Length) =>
|
export const formatDateFromPostgresString = (
|
||||||
|
date: string,
|
||||||
|
length?: Length,
|
||||||
|
) =>
|
||||||
formatDate({
|
formatDate({
|
||||||
date: parse(date, DATE_STRING_FORMAT_POSTGRES, new Date()),
|
date: parse(date, DATE_STRING_FORMAT_POSTGRES, new Date()),
|
||||||
length,
|
length,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user