Mobile Sidebar (#330)
* Show top entities on mobile * Add config * Localize 'more'/'less' text
This commit is contained in:
parent
64c4b21f75
commit
3b6001602a
@ -138,7 +138,8 @@ Application behavior can be changed by configuring the following environment var
|
|||||||
- `recipes` (default)
|
- `recipes` (default)
|
||||||
- `films` (default)
|
- `films` (default)
|
||||||
- `focal-lengths`
|
- `focal-lengths`
|
||||||
- `NEXT_PUBLIC_HIDE_CATEGORY_IMAGE_HOVERS = 1` prevents images displaying when hovering over category links:
|
- `NEXT_PUBLIC_HIDE_CATEGORIES_ON_MOBILE = 1` prevents categories displaying on mobile grid view
|
||||||
|
- `NEXT_PUBLIC_HIDE_CATEGORY_IMAGE_HOVERS = 1` prevents images displaying when hovering over category links
|
||||||
- `NEXT_PUBLIC_EXHAUSTIVE_SIDEBAR_CATEGORIES = 1` always shows expanded sidebar content
|
- `NEXT_PUBLIC_EXHAUSTIVE_SIDEBAR_CATEGORIES = 1` always shows expanded sidebar content
|
||||||
- `NEXT_PUBLIC_HIDE_TAGS_WITH_ONE_PHOTO = 1` to only show tags with 2 or more photos
|
- `NEXT_PUBLIC_HIDE_TAGS_WITH_ONE_PHOTO = 1` to only show tags with 2 or more photos
|
||||||
|
|
||||||
|
|||||||
18
package.json
18
package.json
@ -10,10 +10,10 @@
|
|||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.17.1",
|
"packageManager": "pnpm@10.17.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/openai": "^2.0.38",
|
"@ai-sdk/openai": "^2.0.40",
|
||||||
"@ai-sdk/rsc": "^1.0.56",
|
"@ai-sdk/rsc": "^1.0.59",
|
||||||
"@aws-sdk/client-s3": "3.896.0",
|
"@aws-sdk/client-s3": "3.899.0",
|
||||||
"@aws-sdk/s3-request-presigner": "3.896.0",
|
"@aws-sdk/s3-request-presigner": "3.899.0",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
@ -24,7 +24,7 @@
|
|||||||
"@vercel/analytics": "^1.5.0",
|
"@vercel/analytics": "^1.5.0",
|
||||||
"@vercel/blob": "^2.0.0",
|
"@vercel/blob": "^2.0.0",
|
||||||
"@vercel/speed-insights": "^1.2.0",
|
"@vercel/speed-insights": "^1.2.0",
|
||||||
"ai": "^5.0.56",
|
"ai": "^5.0.59",
|
||||||
"camelcase-keys": "^10.0.0",
|
"camelcase-keys": "^10.0.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
@ -65,17 +65,17 @@
|
|||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
"@types/culori": "^4.0.1",
|
"@types/culori": "^4.0.1",
|
||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^30.0.0",
|
||||||
"@types/node": "^24.5.2",
|
"@types/node": "^24.6.0",
|
||||||
"@types/pg": "^8.15.5",
|
"@types/pg": "^8.15.5",
|
||||||
"@types/react": "19.1.14",
|
"@types/react": "19.1.15",
|
||||||
"@types/react-dom": "19.1.9",
|
"@types/react-dom": "19.1.9",
|
||||||
"@types/sanitize-html": "^2.16.0",
|
"@types/sanitize-html": "^2.16.0",
|
||||||
"cross-fetch": "^4.1.0",
|
"cross-fetch": "^4.1.0",
|
||||||
"eslint": "9.36.0",
|
"eslint": "9.36.0",
|
||||||
"eslint-config-next": "15.5.4",
|
"eslint-config-next": "15.5.4",
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"jest": "^30.1.3",
|
"jest": "^30.2.0",
|
||||||
"jest-environment-jsdom": "^30.1.2",
|
"jest-environment-jsdom": "^30.2.0",
|
||||||
"postcss": "8.5.6",
|
"postcss": "8.5.6",
|
||||||
"tailwindcss": "4.1.13",
|
"tailwindcss": "4.1.13",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
|
|||||||
1422
pnpm-lock.yaml
generated
1422
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -24,7 +24,7 @@ export default function SignInOrUploadClient({
|
|||||||
)}>
|
)}>
|
||||||
<div>
|
<div>
|
||||||
{isCheckingAuth
|
{isCheckingAuth
|
||||||
? appText.misc.loading
|
? appText.utility.loading
|
||||||
: isUserSignedIn
|
: isUserSignedIn
|
||||||
? appText.onboarding.setupFirstPhoto
|
? appText.onboarding.setupFirstPhoto
|
||||||
: appText.onboarding.setupSignIn}
|
: appText.onboarding.setupSignIn}
|
||||||
|
|||||||
@ -88,6 +88,7 @@ export default function AdminAppConfigurationClient({
|
|||||||
// Categories
|
// Categories
|
||||||
hasCategoryVisibility,
|
hasCategoryVisibility,
|
||||||
categoryVisibility,
|
categoryVisibility,
|
||||||
|
showCategoriesOnMobile,
|
||||||
showCategoryImageHover,
|
showCategoryImageHover,
|
||||||
collapseSidebarCategories,
|
collapseSidebarCategories,
|
||||||
hideTagsWithOnePhoto,
|
hideTagsWithOnePhoto,
|
||||||
@ -613,6 +614,19 @@ export default function AdminAppConfigurationClient({
|
|||||||
</div>
|
</div>
|
||||||
{renderEnvVars(['NEXT_PUBLIC_CATEGORY_VISIBILITY'])}
|
{renderEnvVars(['NEXT_PUBLIC_CATEGORY_VISIBILITY'])}
|
||||||
</ChecklistRow>
|
</ChecklistRow>
|
||||||
|
<ChecklistRow
|
||||||
|
title="Show on mobile"
|
||||||
|
status={showCategoriesOnMobile}
|
||||||
|
optional
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div>
|
||||||
|
Set environment variable to {'"1"'} to prevent categories
|
||||||
|
displaying on mobile grid view:
|
||||||
|
{renderEnvVars(['NEXT_PUBLIC_HIDE_CATEGORIES_ON_MOBILE'])}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ChecklistRow>
|
||||||
<ChecklistRow
|
<ChecklistRow
|
||||||
title="Show image hovers"
|
title="Show image hovers"
|
||||||
status={showCategoryImageHover}
|
status={showCategoryImageHover}
|
||||||
|
|||||||
@ -284,6 +284,8 @@ export const SHOW_FILMS =
|
|||||||
CATEGORY_VISIBILITY.includes('films');
|
CATEGORY_VISIBILITY.includes('films');
|
||||||
export const SHOW_FOCAL_LENGTHS =
|
export const SHOW_FOCAL_LENGTHS =
|
||||||
CATEGORY_VISIBILITY.includes('focal-lengths');
|
CATEGORY_VISIBILITY.includes('focal-lengths');
|
||||||
|
export const SHOW_CATEGORIES_ON_MOBILE =
|
||||||
|
process.env.NEXT_PUBLIC_HIDE_CATEGORIES_ON_MOBILE !== '1';
|
||||||
export const SHOW_CATEGORY_IMAGE_HOVERS =
|
export const SHOW_CATEGORY_IMAGE_HOVERS =
|
||||||
process.env.NEXT_PUBLIC_HIDE_CATEGORY_IMAGE_HOVERS !== '1';
|
process.env.NEXT_PUBLIC_HIDE_CATEGORY_IMAGE_HOVERS !== '1';
|
||||||
export const COLLAPSE_SIDEBAR_CATEGORIES =
|
export const COLLAPSE_SIDEBAR_CATEGORIES =
|
||||||
@ -454,6 +456,7 @@ export const APP_CONFIGURATION = {
|
|||||||
hasCategoryVisibility:
|
hasCategoryVisibility:
|
||||||
Boolean(process.env.NEXT_PUBLIC_CATEGORY_VISIBILITY),
|
Boolean(process.env.NEXT_PUBLIC_CATEGORY_VISIBILITY),
|
||||||
categoryVisibility: CATEGORY_VISIBILITY,
|
categoryVisibility: CATEGORY_VISIBILITY,
|
||||||
|
showCategoriesOnMobile: SHOW_CATEGORIES_ON_MOBILE,
|
||||||
showCategoryImageHover: SHOW_CATEGORY_IMAGE_HOVERS,
|
showCategoryImageHover: SHOW_CATEGORY_IMAGE_HOVERS,
|
||||||
collapseSidebarCategories: COLLAPSE_SIDEBAR_CATEGORIES,
|
collapseSidebarCategories: COLLAPSE_SIDEBAR_CATEGORIES,
|
||||||
hideTagsWithOnePhoto: HIDE_TAGS_WITH_ONE_PHOTO,
|
hideTagsWithOnePhoto: HIDE_TAGS_WITH_ONE_PHOTO,
|
||||||
|
|||||||
49
src/category/mobile.ts
Normal file
49
src/category/mobile.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { getTopNonFavTags, tagsHaveFavs } from '@/tag';
|
||||||
|
import { PhotoSetCategories } from '@/category';
|
||||||
|
import {
|
||||||
|
SHOW_ALBUMS,
|
||||||
|
SHOW_CAMERAS,
|
||||||
|
SHOW_FILMS,
|
||||||
|
SHOW_FOCAL_LENGTHS,
|
||||||
|
SHOW_LENSES,
|
||||||
|
SHOW_RECENTS,
|
||||||
|
SHOW_RECIPES,
|
||||||
|
SHOW_TAGS,
|
||||||
|
} from '@/app/config';
|
||||||
|
|
||||||
|
const MAX_ALBUM_TAG_COUNT = 3;
|
||||||
|
const MINIMUM_TOP_ENTITIES = 3;
|
||||||
|
|
||||||
|
export const getTopEntities = ({
|
||||||
|
tags,
|
||||||
|
recents,
|
||||||
|
albums,
|
||||||
|
recipes,
|
||||||
|
films,
|
||||||
|
focalLengths,
|
||||||
|
cameras,
|
||||||
|
lenses,
|
||||||
|
}: PhotoSetCategories) => ({
|
||||||
|
hasFavs: tagsHaveFavs(tags),
|
||||||
|
hasRecents: SHOW_RECENTS && recents.length > 0,
|
||||||
|
albums: SHOW_ALBUMS ? albums.slice(0, MAX_ALBUM_TAG_COUNT) : [],
|
||||||
|
tags: SHOW_TAGS ? getTopNonFavTags(tags).slice(0, MAX_ALBUM_TAG_COUNT) : [],
|
||||||
|
recipe: SHOW_RECIPES ? recipes[0]?.recipe : undefined,
|
||||||
|
film: SHOW_FILMS ? films[0]?.film : undefined,
|
||||||
|
focal: SHOW_FOCAL_LENGTHS ? focalLengths[0]?.focal : undefined,
|
||||||
|
camera: SHOW_CAMERAS ? cameras[0]?.camera : undefined,
|
||||||
|
lens: SHOW_LENSES ? lenses[0]?.lens : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const hasEnoughTopEntities = (categories: PhotoSetCategories) => {
|
||||||
|
const entityCount = Object.values(getTopEntities(categories))
|
||||||
|
.reduce<number>((acc, entity) => {
|
||||||
|
if (Array.isArray(entity)) {
|
||||||
|
return acc + entity.length;
|
||||||
|
} else {
|
||||||
|
return Boolean(entity) ? acc + 1 : acc;
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
return entityCount > MINIMUM_TOP_ENTITIES;
|
||||||
|
};
|
||||||
@ -7,7 +7,7 @@ import { Album } from '@/album';
|
|||||||
export default function useCategoryCounts() {
|
export default function useCategoryCounts() {
|
||||||
const { categoriesWithCounts } = useAppState();
|
const { categoriesWithCounts } = useAppState();
|
||||||
|
|
||||||
const recentsCount = categoriesWithCounts?.recents[0] ?? 0;
|
const recentsCount = categoriesWithCounts?.recents?.count ?? 0;
|
||||||
|
|
||||||
const getYearsCount = useCallback((year: string) => {
|
const getYearsCount = useCallback((year: string) => {
|
||||||
const yearCounts = categoriesWithCounts?.years ?? {};
|
const yearCounts = categoriesWithCounts?.years ?? {};
|
||||||
|
|||||||
@ -764,12 +764,11 @@ export default function CommandKClient({
|
|||||||
? <span className="translate-y-[2px]">
|
? <span className="translate-y-[2px]">
|
||||||
<Spinner size={16} className="-mr-1" />
|
<Spinner size={16} className="-mr-1" />
|
||||||
</span>
|
</span>
|
||||||
: <span className="max-sm:hidden">
|
: <span>
|
||||||
<LoaderButton
|
<LoaderButton
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'h-auto! py-1 -mr-2',
|
'h-auto! py-1 mr-[-9px]',
|
||||||
'border-medium shadow-none',
|
'px-1',
|
||||||
queryLiveRaw ? 'px-1' : 'px-1.5',
|
|
||||||
'text-[12px]',
|
'text-[12px]',
|
||||||
'text-gray-400/90 dark:text-gray-700',
|
'text-gray-400/90 dark:text-gray-700',
|
||||||
)}
|
)}
|
||||||
@ -784,7 +783,14 @@ export default function CommandKClient({
|
|||||||
>
|
>
|
||||||
{queryLiveRaw
|
{queryLiveRaw
|
||||||
? <IoClose size={17} className="text-dim" />
|
? <IoClose size={17} className="text-dim" />
|
||||||
: 'ESC'}
|
: <>
|
||||||
|
<span className="sm:hidden">
|
||||||
|
<IoClose size={17} className="text-dim" />
|
||||||
|
</span>
|
||||||
|
<span className="max-sm:hidden mx-0.5">
|
||||||
|
ESC
|
||||||
|
</span>
|
||||||
|
</>}
|
||||||
</LoaderButton>
|
</LoaderButton>
|
||||||
</span>}
|
</span>}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -11,7 +11,7 @@ export default function Badge({
|
|||||||
}: {
|
}: {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
className?: string
|
className?: string
|
||||||
type?: 'large' | 'small' | 'text-only'
|
type?: 'large' | 'medium' | 'small' |'text-only'
|
||||||
dimContent?: boolean
|
dimContent?: boolean
|
||||||
contrast?: 'low' | 'medium' | 'high' | 'frosted'
|
contrast?: 'low' | 'medium' | 'high' | 'frosted'
|
||||||
uppercase?: boolean
|
uppercase?: boolean
|
||||||
@ -27,14 +27,19 @@ export default function Badge({
|
|||||||
'border border-medium',
|
'border border-medium',
|
||||||
);
|
);
|
||||||
case 'small':
|
case 'small':
|
||||||
|
case 'medium':
|
||||||
return clsx(
|
return clsx(
|
||||||
'px-[5px] h-[17px] md:h-[18px]',
|
type === 'small'
|
||||||
'text-[0.7rem] font-medium rounded-md',
|
? 'px-[5px] h-[17px] md:h-[18px]'
|
||||||
|
: 'px-2 h-6.5',
|
||||||
|
type === 'small'
|
||||||
|
? 'text-[0.7rem] font-medium rounded-md'
|
||||||
|
: 'text-[0.9rem] rounded-lg',
|
||||||
contrast === 'high'
|
contrast === 'high'
|
||||||
? 'text-invert bg-invert'
|
? 'text-invert bg-invert'
|
||||||
: contrast === 'frosted'
|
: contrast === 'frosted'
|
||||||
? 'text-black bg-neutral-100/30 border border-neutral-200/40'
|
? 'text-black bg-neutral-100/30 border border-neutral-200/40'
|
||||||
: 'text-medium bg-gray-300/30 dark:bg-gray-700/50',
|
: 'text-medium-dark bg-gray-300/30 dark:bg-gray-700/50',
|
||||||
interactive && (contrast === 'high'
|
interactive && (contrast === 'high'
|
||||||
? 'hover:opacity-70'
|
? 'hover:opacity-70'
|
||||||
: contrast === 'frosted'
|
: contrast === 'frosted'
|
||||||
@ -53,7 +58,8 @@ export default function Badge({
|
|||||||
'max-w-full',
|
'max-w-full',
|
||||||
'inline-flex items-center',
|
'inline-flex items-center',
|
||||||
stylesForType(),
|
stylesForType(),
|
||||||
uppercase && 'uppercase tracking-wider',
|
uppercase && 'uppercase',
|
||||||
|
uppercase && type !== 'medium' && 'tracking-wider',
|
||||||
className,
|
className,
|
||||||
)}>
|
)}>
|
||||||
<span className={clsx(
|
<span className={clsx(
|
||||||
|
|||||||
@ -31,7 +31,7 @@ export default function CopyButton({
|
|||||||
onClick={text
|
onClick={text
|
||||||
? () => {
|
? () => {
|
||||||
navigator.clipboard.writeText(text);
|
navigator.clipboard.writeText(text);
|
||||||
toastSuccess(appText.misc.copyPhrase(label));
|
toastSuccess(appText.utility.copyPhrase(label));
|
||||||
}
|
}
|
||||||
: undefined}
|
: undefined}
|
||||||
styleAs="link"
|
styleAs="link"
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { ReactNode, useState } from 'react';
|
|||||||
import LoaderButton from './primitives/LoaderButton';
|
import LoaderButton from './primitives/LoaderButton';
|
||||||
import { IoChevronDownOutline, IoChevronUpOutline } from 'react-icons/io5';
|
import { IoChevronDownOutline, IoChevronUpOutline } from 'react-icons/io5';
|
||||||
import { COLLAPSE_SIDEBAR_CATEGORIES } from '@/app/config';
|
import { COLLAPSE_SIDEBAR_CATEGORIES } from '@/app/config';
|
||||||
|
import { useAppText } from '@/i18n/state/client';
|
||||||
|
|
||||||
export default function HeaderList({
|
export default function HeaderList({
|
||||||
title,
|
title,
|
||||||
@ -20,6 +21,8 @@ export default function HeaderList({
|
|||||||
items: ReactNode[],
|
items: ReactNode[],
|
||||||
maxItems?: number,
|
maxItems?: number,
|
||||||
}) {
|
}) {
|
||||||
|
const { utility } = useAppText();
|
||||||
|
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
|
||||||
const hasItemsToExpand =
|
const hasItemsToExpand =
|
||||||
@ -70,11 +73,11 @@ export default function HeaderList({
|
|||||||
'group',
|
'group',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{<span className="flex items-center gap-1">
|
{<span className="flex items-center gap-1 uppercase">
|
||||||
{isExpanded
|
{isExpanded
|
||||||
? 'LESS'
|
? utility.less
|
||||||
: <>
|
: <>
|
||||||
MORE
|
{utility.more}
|
||||||
<span className="hidden group-hover:inline text-dim!">
|
<span className="hidden group-hover:inline text-dim!">
|
||||||
{' '}
|
{' '}
|
||||||
{items.length - maxItems}
|
{items.length - maxItems}
|
||||||
|
|||||||
@ -5,11 +5,12 @@ 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();
|
const { footer } = 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">
|
||||||
{appText.misc.repo}
|
{footer.madeWith}
|
||||||
</span>
|
</span>
|
||||||
<Link
|
<Link
|
||||||
href={TEMPLATE_REPO_URL}
|
href={TEMPLATE_REPO_URL}
|
||||||
|
|||||||
@ -17,6 +17,7 @@ export interface EntityLinkExternalProps {
|
|||||||
ref?: RefObject<HTMLSpanElement | null>
|
ref?: RefObject<HTMLSpanElement | null>
|
||||||
type?: LabeledIconType
|
type?: LabeledIconType
|
||||||
badged?: boolean
|
badged?: boolean
|
||||||
|
badgeType?: ComponentProps<typeof Badge>['type']
|
||||||
contrast?: ComponentProps<typeof Badge>['contrast']
|
contrast?: ComponentProps<typeof Badge>['contrast']
|
||||||
uppercase?: boolean
|
uppercase?: boolean
|
||||||
prefetch?: boolean
|
prefetch?: boolean
|
||||||
@ -38,6 +39,7 @@ export default function EntityLink({
|
|||||||
iconWide,
|
iconWide,
|
||||||
type,
|
type,
|
||||||
badged,
|
badged,
|
||||||
|
badgeType = 'small',
|
||||||
contrast = 'medium',
|
contrast = 'medium',
|
||||||
path = '', // Make link optional for debugging purposes
|
path = '', // Make link optional for debugging purposes
|
||||||
hoverCount = 0,
|
hoverCount = 0,
|
||||||
@ -70,7 +72,11 @@ export default function EntityLink({
|
|||||||
} & EntityLinkExternalProps) {
|
} & EntityLinkExternalProps) {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const hasBadgeIcon = Boolean(iconBadgeStart || iconBadgeEnd);
|
const hasBadgeIcon = Boolean(
|
||||||
|
iconBadgeStart ||
|
||||||
|
iconBadgeEnd ||
|
||||||
|
badgeType === 'medium',
|
||||||
|
);
|
||||||
|
|
||||||
const classForContrast = () => {
|
const classForContrast = () => {
|
||||||
switch (contrast) {
|
switch (contrast) {
|
||||||
@ -121,10 +127,12 @@ export default function EntityLink({
|
|||||||
setIsLoading={setIsLoading}
|
setIsLoading={setIsLoading}
|
||||||
>
|
>
|
||||||
<LabeledIcon {...{
|
<LabeledIcon {...{
|
||||||
icon:
|
icon: badged && hasBadgeIcon && !useForHover
|
||||||
(badged && hasBadgeIcon && !useForHover) ? undefined : icon,
|
? undefined
|
||||||
iconWide:
|
: icon,
|
||||||
(badged && hasBadgeIcon && !useForHover) ? undefined : iconWide,
|
iconWide: badged && hasBadgeIcon && !useForHover
|
||||||
|
? undefined
|
||||||
|
: iconWide,
|
||||||
prefetch,
|
prefetch,
|
||||||
title,
|
title,
|
||||||
type: useForHover ? 'icon-first' : type,
|
type: useForHover ? 'icon-first' : type,
|
||||||
@ -138,18 +146,24 @@ export default function EntityLink({
|
|||||||
}}>
|
}}>
|
||||||
{badged && !useForHover
|
{badged && !useForHover
|
||||||
? <Badge
|
? <Badge
|
||||||
type="small"
|
type={badgeType}
|
||||||
contrast={contrast}
|
contrast={contrast}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'translate-y-[-0.5px]',
|
'translate-y-[-0.5px]',
|
||||||
hasBadgeIcon && '*:flex *:items-center *:gap-1',
|
hasBadgeIcon && '*:flex *:items-center',
|
||||||
|
hasBadgeIcon && badgeType === 'medium'
|
||||||
|
? '*:gap-[5px]'
|
||||||
|
: '*:gap-1',
|
||||||
|
suppressSpinner && isLoading && 'opacity-50',
|
||||||
)}
|
)}
|
||||||
uppercase
|
uppercase
|
||||||
interactive
|
interactive
|
||||||
>
|
>
|
||||||
{iconBadgeStart}
|
{badgeType === 'medium' &&
|
||||||
|
<span className="translate-y-[0.5px]">{icon}</span>}
|
||||||
|
{badgeType !== 'medium' && iconBadgeStart}
|
||||||
{renderLabel}
|
{renderLabel}
|
||||||
{iconBadgeEnd}
|
{badgeType !== 'medium' && iconBadgeEnd}
|
||||||
</Badge>
|
</Badge>
|
||||||
: <span className={clsx(
|
: <span className={clsx(
|
||||||
'text-content',
|
'text-content',
|
||||||
@ -170,6 +184,7 @@ export default function EntityLink({
|
|||||||
'max-w-full overflow-hidden select-none',
|
'max-w-full overflow-hidden select-none',
|
||||||
// Underline link text when action is hovered
|
// Underline link text when action is hovered
|
||||||
'[&:has(.action:hover)_.text-content]:underline',
|
'[&:has(.action:hover)_.text-content]:underline',
|
||||||
|
!truncate && 'shrink-0',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -15,7 +15,7 @@ export default function useMaskedScroll({
|
|||||||
ref: containerRef,
|
ref: containerRef,
|
||||||
direction = 'vertical',
|
direction = 'vertical',
|
||||||
fadeSize = 24,
|
fadeSize = 24,
|
||||||
animationDuration = 0.3,
|
animationDuration = 0.2,
|
||||||
setMaxSize = true,
|
setMaxSize = true,
|
||||||
hideScrollbar = true,
|
hideScrollbar = true,
|
||||||
// Disable when calling 'updateMask' explicitly
|
// Disable when calling 'updateMask' explicitly
|
||||||
|
|||||||
@ -18,7 +18,7 @@ export default function useNavigateOrRunActionWithToast({
|
|||||||
|
|
||||||
const appText = useAppText();
|
const appText = useAppText();
|
||||||
|
|
||||||
const toastMessage = _toastMessage ?? appText.misc.loading;
|
const toastMessage = _toastMessage ?? appText.utility.loading;
|
||||||
|
|
||||||
const toastId = useRef<string | number>(undefined);
|
const toastId = useRef<string | number>(undefined);
|
||||||
|
|
||||||
|
|||||||
@ -55,6 +55,9 @@ export const TEXT: I18N = {
|
|||||||
next: 'পরবর্তী',
|
next: 'পরবর্তী',
|
||||||
nextShort: 'পরবর্তী',
|
nextShort: 'পরবর্তী',
|
||||||
},
|
},
|
||||||
|
footer: {
|
||||||
|
madeWith: 'তৈরি হয়েছে',
|
||||||
|
},
|
||||||
sort: {
|
sort: {
|
||||||
sort: 'সাজান',
|
sort: 'সাজান',
|
||||||
newest: 'নতুনতম',
|
newest: 'নতুনতম',
|
||||||
@ -142,14 +145,15 @@ export const TEXT: I18N = {
|
|||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
setupConfig: 'পরিবেশ ভেরিয়েবল সম্পাদনা করে সাইটের নাম এবং অন্যান্য কনফিগারেশন পরিবর্তন করুন',
|
setupConfig: 'পরিবেশ ভেরিয়েবল সম্পাদনা করে সাইটের নাম এবং অন্যান্য কনফিগারেশন পরিবর্তন করুন',
|
||||||
},
|
},
|
||||||
misc: {
|
utility: {
|
||||||
|
more: 'আরো',
|
||||||
|
less: 'কম',
|
||||||
|
loadMore: 'আরো লোড করুন',
|
||||||
loading: 'লোড হচ্ছে ...',
|
loading: 'লোড হচ্ছে ...',
|
||||||
|
tryAgain: 'আবার চেষ্টা করুন',
|
||||||
finishing: 'সম্পন্ন হচ্ছে ...',
|
finishing: 'সম্পন্ন হচ্ছে ...',
|
||||||
uploading: 'আপলোড হচ্ছে',
|
uploading: 'আপলোড হচ্ছে',
|
||||||
repo: 'তৈরি হয়েছে',
|
|
||||||
copyPhrase: '{{label}} কপি হয়েছে',
|
copyPhrase: '{{label}} কপি হয়েছে',
|
||||||
},
|
|
||||||
utility: {
|
|
||||||
paginate: '{{index}} / {{count}}',
|
paginate: '{{index}} / {{count}}',
|
||||||
paginateAction: '{{action}} - {{index}} / {{count}}',
|
paginateAction: '{{action}} - {{index}} / {{count}}',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -55,6 +55,9 @@ export const TEXT: I18N = {
|
|||||||
next: 'Next',
|
next: 'Next',
|
||||||
nextShort: 'Next',
|
nextShort: 'Next',
|
||||||
},
|
},
|
||||||
|
footer: {
|
||||||
|
madeWith: 'Made with',
|
||||||
|
},
|
||||||
sort: {
|
sort: {
|
||||||
sort: 'Sort',
|
sort: 'Sort',
|
||||||
newest: 'Newest',
|
newest: 'Newest',
|
||||||
@ -142,14 +145,15 @@ export const TEXT: I18N = {
|
|||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
setupConfig: 'Change the site name and other configuration by editing environment variables referenced in',
|
setupConfig: 'Change the site name and other configuration by editing environment variables referenced in',
|
||||||
},
|
},
|
||||||
misc: {
|
utility: {
|
||||||
|
more: 'More',
|
||||||
|
less: 'Less',
|
||||||
|
loadMore: 'Load More',
|
||||||
loading: 'Loading ...',
|
loading: 'Loading ...',
|
||||||
|
tryAgain: 'Try Again',
|
||||||
finishing: 'Finishing ...',
|
finishing: 'Finishing ...',
|
||||||
uploading: 'Uploading',
|
uploading: 'Uploading',
|
||||||
repo: 'Made with',
|
|
||||||
copyPhrase: '{{label}} copied',
|
copyPhrase: '{{label}} copied',
|
||||||
},
|
|
||||||
utility: {
|
|
||||||
paginate: '{{index}} of {{count}}',
|
paginate: '{{index}} of {{count}}',
|
||||||
paginateAction: '{{action}} {{index}} of {{count}}',
|
paginateAction: '{{action}} {{index}} of {{count}}',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -54,6 +54,9 @@ export const TEXT = {
|
|||||||
next: 'Next',
|
next: 'Next',
|
||||||
nextShort: 'Next',
|
nextShort: 'Next',
|
||||||
},
|
},
|
||||||
|
footer: {
|
||||||
|
madeWith: 'Made with',
|
||||||
|
},
|
||||||
sort: {
|
sort: {
|
||||||
sort: 'Sort',
|
sort: 'Sort',
|
||||||
newest: 'Newest',
|
newest: 'Newest',
|
||||||
@ -141,14 +144,15 @@ export const TEXT = {
|
|||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
setupConfig: 'Change the site name and other configuration by editing environment variables referenced in',
|
setupConfig: 'Change the site name and other configuration by editing environment variables referenced in',
|
||||||
},
|
},
|
||||||
misc: {
|
utility: {
|
||||||
|
more: 'More',
|
||||||
|
less: 'Less',
|
||||||
|
loadMore: 'Load More',
|
||||||
loading: 'Loading ...',
|
loading: 'Loading ...',
|
||||||
|
tryAgain: 'Try Again',
|
||||||
finishing: 'Finishing ...',
|
finishing: 'Finishing ...',
|
||||||
uploading: 'Uploading',
|
uploading: 'Uploading',
|
||||||
repo: 'Made with',
|
|
||||||
copyPhrase: '{{label}} copied',
|
copyPhrase: '{{label}} copied',
|
||||||
},
|
|
||||||
utility: {
|
|
||||||
paginate: '{{index}} of {{count}}',
|
paginate: '{{index}} of {{count}}',
|
||||||
paginateAction: '{{action}} {{index}} of {{count}}',
|
paginateAction: '{{action}} {{index}} of {{count}}',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -55,6 +55,9 @@ export const TEXT: I18N = {
|
|||||||
next: 'अगला',
|
next: 'अगला',
|
||||||
nextShort: 'अगला',
|
nextShort: 'अगला',
|
||||||
},
|
},
|
||||||
|
footer: {
|
||||||
|
madeWith: 'निर्मित',
|
||||||
|
},
|
||||||
sort: {
|
sort: {
|
||||||
sort: 'क्रमबद्ध करें',
|
sort: 'क्रमबद्ध करें',
|
||||||
newest: 'नवीनतम',
|
newest: 'नवीनतम',
|
||||||
@ -143,14 +146,15 @@ export const TEXT: I18N = {
|
|||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
setupConfig: 'साइट का नाम और अन्य कॉन्फ़िगरेशन बदलने के लिए पर्यावरण चर संपादित करें',
|
setupConfig: 'साइट का नाम और अन्य कॉन्फ़िगरेशन बदलने के लिए पर्यावरण चर संपादित करें',
|
||||||
},
|
},
|
||||||
misc: {
|
utility: {
|
||||||
|
more: 'और',
|
||||||
|
less: 'कम',
|
||||||
|
loadMore: 'और लोड करें',
|
||||||
loading: 'लोड हो रहा है...',
|
loading: 'लोड हो रहा है...',
|
||||||
|
tryAgain: 'फिर से कोशिश करें',
|
||||||
finishing: 'समाप्त कर रहे हैं...',
|
finishing: 'समाप्त कर रहे हैं...',
|
||||||
uploading: 'अपलोड हो रहा है',
|
uploading: 'अपलोड हो रहा है',
|
||||||
repo: 'निर्मित',
|
|
||||||
copyPhrase: '{{label}} कॉपी किया गया',
|
copyPhrase: '{{label}} कॉपी किया गया',
|
||||||
},
|
|
||||||
utility: {
|
|
||||||
paginate: '{{index}} / {{count}}',
|
paginate: '{{index}} / {{count}}',
|
||||||
paginateAction: '{{action}} - {{index}} / {{count}}',
|
paginateAction: '{{action}} - {{index}} / {{count}}',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -55,6 +55,9 @@ export const TEXT: I18N = {
|
|||||||
next: 'Berikutnya',
|
next: 'Berikutnya',
|
||||||
nextShort: 'Brkt',
|
nextShort: 'Brkt',
|
||||||
},
|
},
|
||||||
|
footer: {
|
||||||
|
madeWith: 'Dibuat dengan',
|
||||||
|
},
|
||||||
sort: {
|
sort: {
|
||||||
sort: 'Urutkan',
|
sort: 'Urutkan',
|
||||||
newest: 'Terbaru',
|
newest: 'Terbaru',
|
||||||
@ -141,14 +144,15 @@ export const TEXT: I18N = {
|
|||||||
setupFirstPhoto: 'Tambahkan foto pertama Anda',
|
setupFirstPhoto: 'Tambahkan foto pertama Anda',
|
||||||
setupConfig: 'Ubah nama situs dan pengaturan lewat file environment',
|
setupConfig: 'Ubah nama situs dan pengaturan lewat file environment',
|
||||||
},
|
},
|
||||||
misc: {
|
utility: {
|
||||||
|
more: 'Lebih banyak',
|
||||||
|
less: 'Lebih sedikit',
|
||||||
|
loadMore: 'Muat Lebih Banyak',
|
||||||
loading: 'Memuat ...',
|
loading: 'Memuat ...',
|
||||||
|
tryAgain: 'Coba Lagi',
|
||||||
finishing: 'Menyelesaikan ...',
|
finishing: 'Menyelesaikan ...',
|
||||||
uploading: 'Mengunggah',
|
uploading: 'Mengunggah',
|
||||||
repo: 'Dibuat dengan',
|
|
||||||
copyPhrase: '{{label}} disalin',
|
copyPhrase: '{{label}} disalin',
|
||||||
},
|
|
||||||
utility: {
|
|
||||||
paginate: '{{index}} dari {{count}}',
|
paginate: '{{index}} dari {{count}}',
|
||||||
paginateAction: '{{action}} {{index}} dari {{count}}',
|
paginateAction: '{{action}} {{index}} dari {{count}}',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -55,6 +55,9 @@ export const TEXT: I18N = {
|
|||||||
next: 'Próximo',
|
next: 'Próximo',
|
||||||
nextShort: 'Próx',
|
nextShort: 'Próx',
|
||||||
},
|
},
|
||||||
|
footer: {
|
||||||
|
madeWith: 'Feito com',
|
||||||
|
},
|
||||||
sort: {
|
sort: {
|
||||||
sort: 'Ordenar',
|
sort: 'Ordenar',
|
||||||
newest: 'Mais recentes',
|
newest: 'Mais recentes',
|
||||||
@ -142,14 +145,15 @@ export const TEXT: I18N = {
|
|||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
setupConfig: 'Altere o nome do site e outras configurações editando as variáveis de ambiente referenciadas em',
|
setupConfig: 'Altere o nome do site e outras configurações editando as variáveis de ambiente referenciadas em',
|
||||||
},
|
},
|
||||||
misc: {
|
utility: {
|
||||||
|
more: 'Mais',
|
||||||
|
less: 'Menos',
|
||||||
|
loadMore: 'Carregar Mais',
|
||||||
loading: 'Carregando ...',
|
loading: 'Carregando ...',
|
||||||
|
tryAgain: 'Tentar Novamente',
|
||||||
finishing: 'Finalizando ...',
|
finishing: 'Finalizando ...',
|
||||||
uploading: 'Enviando',
|
uploading: 'Enviando',
|
||||||
repo: 'Feito com',
|
|
||||||
copyPhrase: '{{label}} copiado',
|
copyPhrase: '{{label}} copiado',
|
||||||
},
|
|
||||||
utility: {
|
|
||||||
paginate: '{{index}} de {{count}}',
|
paginate: '{{index}} de {{count}}',
|
||||||
paginateAction: '{{action}} {{index}} de {{count}}',
|
paginateAction: '{{action}} {{index}} de {{count}}',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -55,6 +55,9 @@ export const TEXT: I18N = {
|
|||||||
next: 'Próximo',
|
next: 'Próximo',
|
||||||
nextShort: 'Próx',
|
nextShort: 'Próx',
|
||||||
},
|
},
|
||||||
|
footer: {
|
||||||
|
madeWith: 'Feito com',
|
||||||
|
},
|
||||||
sort: {
|
sort: {
|
||||||
sort: 'Ordenar',
|
sort: 'Ordenar',
|
||||||
newest: 'Mais recentes',
|
newest: 'Mais recentes',
|
||||||
@ -142,14 +145,15 @@ export const TEXT: I18N = {
|
|||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
setupConfig: 'Altere o nome do sítio e outras configurações ao editar as variáveis de ambiente referenciadas em',
|
setupConfig: 'Altere o nome do sítio e outras configurações ao editar as variáveis de ambiente referenciadas em',
|
||||||
},
|
},
|
||||||
misc: {
|
utility: {
|
||||||
|
more: 'Mais',
|
||||||
|
less: 'Menos',
|
||||||
|
loadMore: 'Carregar Mais',
|
||||||
loading: 'A carregar ...',
|
loading: 'A carregar ...',
|
||||||
|
tryAgain: 'Tentar Novamente',
|
||||||
finishing: 'A finalizar ...',
|
finishing: 'A finalizar ...',
|
||||||
uploading: 'A enviar',
|
uploading: 'A enviar',
|
||||||
repo: 'Feito com',
|
|
||||||
copyPhrase: '{{label}} copiado',
|
copyPhrase: '{{label}} copiado',
|
||||||
},
|
|
||||||
utility: {
|
|
||||||
paginate: '{{index}} de {{count}}',
|
paginate: '{{index}} de {{count}}',
|
||||||
paginateAction: '{{action}} {{index}} de {{count}}',
|
paginateAction: '{{action}} {{index}} de {{count}}',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -55,6 +55,9 @@ export const TEXT: I18N = {
|
|||||||
next: 'Sonraki',
|
next: 'Sonraki',
|
||||||
nextShort: 'Sonraki',
|
nextShort: 'Sonraki',
|
||||||
},
|
},
|
||||||
|
footer: {
|
||||||
|
madeWith: 'Hazırlayan:',
|
||||||
|
},
|
||||||
sort: {
|
sort: {
|
||||||
sort: 'Sırala',
|
sort: 'Sırala',
|
||||||
newest: 'En Yeni',
|
newest: 'En Yeni',
|
||||||
@ -143,14 +146,15 @@ export const TEXT: I18N = {
|
|||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
setupConfig: 'Site adını ve diğer ayarları değiştirmek için şu ortam değişkenlerini düzenleyin:',
|
setupConfig: 'Site adını ve diğer ayarları değiştirmek için şu ortam değişkenlerini düzenleyin:',
|
||||||
},
|
},
|
||||||
misc: {
|
utility: {
|
||||||
|
more: 'Daha fazla',
|
||||||
|
less: 'Daha az',
|
||||||
|
loadMore: 'Daha Fazla Yükle',
|
||||||
loading: 'Yükleniyor ...',
|
loading: 'Yükleniyor ...',
|
||||||
|
tryAgain: 'Tekrar Dene',
|
||||||
finishing: 'Tamamlanıyor ...',
|
finishing: 'Tamamlanıyor ...',
|
||||||
uploading: 'Yükleniyor',
|
uploading: 'Yükleniyor',
|
||||||
repo: 'Hazırlayan:',
|
|
||||||
copyPhrase: '{{label}} kopyalandı',
|
copyPhrase: '{{label}} kopyalandı',
|
||||||
},
|
|
||||||
utility: {
|
|
||||||
paginate: '{{count}} fotoğrafın {{index}}.si',
|
paginate: '{{count}} fotoğrafın {{index}}.si',
|
||||||
paginateAction: '{{action}} - {{count}} fotoğrafın {{index}}.si',
|
paginateAction: '{{action}} - {{count}} fotoğrafın {{index}}.si',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -55,6 +55,9 @@ export const TEXT: I18N = {
|
|||||||
next: '下一页',
|
next: '下一页',
|
||||||
nextShort: '下一页',
|
nextShort: '下一页',
|
||||||
},
|
},
|
||||||
|
footer: {
|
||||||
|
madeWith: '基于',
|
||||||
|
},
|
||||||
sort: {
|
sort: {
|
||||||
sort: '排序',
|
sort: '排序',
|
||||||
newest: '最新',
|
newest: '最新',
|
||||||
@ -141,14 +144,15 @@ export const TEXT: I18N = {
|
|||||||
setupFirstPhoto: '添加您的第一张照片',
|
setupFirstPhoto: '添加您的第一张照片',
|
||||||
setupConfig: '通过编辑环境变量来更改站点名称和其他配置',
|
setupConfig: '通过编辑环境变量来更改站点名称和其他配置',
|
||||||
},
|
},
|
||||||
misc: {
|
utility: {
|
||||||
|
more: '更多',
|
||||||
|
less: '更少',
|
||||||
|
loadMore: '加载更多',
|
||||||
loading: '加载中...',
|
loading: '加载中...',
|
||||||
|
tryAgain: '重试',
|
||||||
finishing: '完成中...',
|
finishing: '完成中...',
|
||||||
uploading: '上传中',
|
uploading: '上传中',
|
||||||
repo: '基于',
|
|
||||||
copyPhrase: '{{label}} 已复制',
|
copyPhrase: '{{label}} 已复制',
|
||||||
},
|
|
||||||
utility: {
|
|
||||||
paginate: '第 {{index}} 页,共 {{count}} 页',
|
paginate: '第 {{index}} 页,共 {{count}} 页',
|
||||||
paginateAction: '{{action}} 第 {{index}} 页,共 {{count}} 页',
|
paginateAction: '{{action}} 第 {{index}} 页,共 {{count}} 页',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -33,13 +33,10 @@ export const generateAppTextState = (i18n: I18N) => {
|
|||||||
deleteConfirm: (photoTitle: string) =>
|
deleteConfirm: (photoTitle: string) =>
|
||||||
i18n.admin.deleteConfirm.replace('{{photoTitle}}', photoTitle),
|
i18n.admin.deleteConfirm.replace('{{photoTitle}}', photoTitle),
|
||||||
},
|
},
|
||||||
misc: {
|
|
||||||
...i18n.misc,
|
|
||||||
copyPhrase: (label: string) =>
|
|
||||||
i18n.misc.copyPhrase.replace('{{label}}', label),
|
|
||||||
},
|
|
||||||
utility: {
|
utility: {
|
||||||
...i18n.utility,
|
...i18n.utility,
|
||||||
|
copyPhrase: (label: string) =>
|
||||||
|
i18n.utility.copyPhrase.replace('{{label}}', label),
|
||||||
paginate: (index: number, count: number) =>
|
paginate: (index: number, count: number) =>
|
||||||
i18n.utility.paginate
|
i18n.utility.paginate
|
||||||
.replace('{{index}}', index.toString())
|
.replace('{{index}}', index.toString())
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import useVisibility from '@/utility/useVisibility';
|
|||||||
import { ADMIN_DB_OPTIMIZE_ENABLED } from '@/app/config';
|
import { ADMIN_DB_OPTIMIZE_ENABLED } from '@/app/config';
|
||||||
import { SortBy } from './sort';
|
import { SortBy } from './sort';
|
||||||
import { SWR_KEYS } from '@/swr';
|
import { SWR_KEYS } from '@/swr';
|
||||||
|
import { useAppText } from '@/i18n/state/client';
|
||||||
|
|
||||||
const SIZE_KEY_SEPARATOR = '__';
|
const SIZE_KEY_SEPARATOR = '__';
|
||||||
const getSizeFromKey = (key: string) =>
|
const getSizeFromKey = (key: string) =>
|
||||||
@ -63,6 +64,8 @@ export default function InfinitePhotoScroll({
|
|||||||
}) => ReactNode
|
}) => ReactNode
|
||||||
} & PhotoSetCategory) {
|
} & PhotoSetCategory) {
|
||||||
const { isUserSignedIn } = useAppState();
|
const { isUserSignedIn } = useAppState();
|
||||||
|
|
||||||
|
const { utility } = useAppText();
|
||||||
|
|
||||||
const keyGenerator = useCallback(
|
const keyGenerator = useCallback(
|
||||||
(size: number, prev: Photo[]) => prev && prev.length === 0
|
(size: number, prev: Photo[]) => prev && prev.length === 0
|
||||||
@ -168,10 +171,10 @@ export default function InfinitePhotoScroll({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{error
|
{error
|
||||||
? 'Try Again'
|
? utility.tryAgain
|
||||||
: isLoadingOrValidating
|
: isLoadingOrValidating
|
||||||
? <Spinner size={20} />
|
? <Spinner size={20} />
|
||||||
: 'Load More'}
|
: utility.loadMore}
|
||||||
</button>
|
</button>
|
||||||
</div>;
|
</div>;
|
||||||
|
|
||||||
|
|||||||
@ -19,6 +19,7 @@ export default function PhotoGridContainer({
|
|||||||
animateOnFirstLoadOnly,
|
animateOnFirstLoadOnly,
|
||||||
header,
|
header,
|
||||||
sidebar,
|
sidebar,
|
||||||
|
className,
|
||||||
...categories
|
...categories
|
||||||
}: {
|
}: {
|
||||||
cacheKey: string
|
cacheKey: string
|
||||||
@ -28,6 +29,7 @@ export default function PhotoGridContainer({
|
|||||||
excludeFromFeeds?: boolean
|
excludeFromFeeds?: boolean
|
||||||
header?: ReactNode
|
header?: ReactNode
|
||||||
sidebar?: ReactNode
|
sidebar?: ReactNode
|
||||||
|
className?: string
|
||||||
} & ComponentProps<typeof PhotoGrid>) {
|
} & ComponentProps<typeof PhotoGrid>) {
|
||||||
const [
|
const [
|
||||||
shouldAnimateDynamicItems,
|
shouldAnimateDynamicItems,
|
||||||
@ -40,6 +42,7 @@ export default function PhotoGridContainer({
|
|||||||
<AppGrid
|
<AppGrid
|
||||||
contentMain={<div className={clsx(
|
contentMain={<div className={clsx(
|
||||||
header && 'space-y-8 mt-1.5',
|
header && 'space-y-8 mt-1.5',
|
||||||
|
className,
|
||||||
)}>
|
)}>
|
||||||
{header &&
|
{header &&
|
||||||
<AnimateItems
|
<AnimateItems
|
||||||
|
|||||||
@ -7,9 +7,12 @@ import PhotoGridContainer from './PhotoGridContainer';
|
|||||||
import { ComponentProps, useMemo, useRef } from 'react';
|
import { ComponentProps, useMemo, useRef } from 'react';
|
||||||
import clsx from 'clsx/lite';
|
import clsx from 'clsx/lite';
|
||||||
import MaskedScroll from '@/components/MaskedScroll';
|
import MaskedScroll from '@/components/MaskedScroll';
|
||||||
import { IS_RECENTS_FIRST } from '@/app/config';
|
import { IS_RECENTS_FIRST, SHOW_CATEGORIES_ON_MOBILE } from '@/app/config';
|
||||||
import { SortBy } from './sort';
|
import { SortBy } from './sort';
|
||||||
import useViewportHeight from '@/utility/useViewportHeight';
|
import useViewportHeight from '@/utility/useViewportHeight';
|
||||||
|
import TopPhotoEntities from './TopPhotoEntities';
|
||||||
|
import AnimateItems from '@/components/AnimateItems';
|
||||||
|
import { hasEnoughTopEntities } from '@/category/mobile';
|
||||||
|
|
||||||
export default function PhotoGridPageClient({
|
export default function PhotoGridPageClient({
|
||||||
photos,
|
photos,
|
||||||
@ -32,34 +35,55 @@ export default function PhotoGridPageClient({
|
|||||||
viewPortHeight - (ref.current?.getBoundingClientRect().y ?? 0),
|
viewPortHeight - (ref.current?.getBoundingClientRect().y ?? 0),
|
||||||
[viewPortHeight]);
|
[viewPortHeight]);
|
||||||
|
|
||||||
|
const shouldShowTopEntities = useMemo(() =>
|
||||||
|
SHOW_CATEGORIES_ON_MOBILE && hasEnoughTopEntities(categories),
|
||||||
|
[categories]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PhotoGridContainer
|
<div>
|
||||||
cacheKey={`page-${PATH_GRID_INFERRED}`}
|
{shouldShowTopEntities &&
|
||||||
photos={photos}
|
<AnimateItems
|
||||||
count={photosCount}
|
type="bottom"
|
||||||
sortBy={sortBy}
|
items={[
|
||||||
sortWithPriority={sortWithPriority}
|
<div key="mobile-sidebar" className={clsx(
|
||||||
excludeFromFeeds
|
'flex gap-x-2',
|
||||||
prioritizeInitialPhotos
|
'md:hidden',
|
||||||
sidebar={
|
'mb-4',
|
||||||
<MaskedScroll
|
)}>
|
||||||
ref={ref}
|
<TopPhotoEntities
|
||||||
className={clsx(
|
className="grow"
|
||||||
'sticky top-0',
|
{...categories}
|
||||||
// Optical adjustment for headerless recents
|
/>
|
||||||
IS_RECENTS_FIRST ? '-mb-4.5 -mt-4.5' : '-mb-5 -mt-5',
|
</div>,
|
||||||
'max-h-screen py-4',
|
]} />}
|
||||||
)}
|
<PhotoGridContainer
|
||||||
fadeSize={100}
|
cacheKey={`page-${PATH_GRID_INFERRED}`}
|
||||||
setMaxSize={false}
|
photos={photos}
|
||||||
>
|
count={photosCount}
|
||||||
<PhotoGridSidebar {...{
|
sortBy={sortBy}
|
||||||
...categories,
|
sortWithPriority={sortWithPriority}
|
||||||
photosCount: photosCountWithExcludes,
|
excludeFromFeeds
|
||||||
containerHeight,
|
prioritizeInitialPhotos
|
||||||
}} />
|
sidebar={
|
||||||
</MaskedScroll>
|
<MaskedScroll
|
||||||
}
|
ref={ref}
|
||||||
/>
|
className={clsx(
|
||||||
|
'sticky top-0',
|
||||||
|
// Optical adjustment for headerless recents
|
||||||
|
IS_RECENTS_FIRST ? '-mb-4.5 -mt-4.5' : '-mb-5 -mt-5',
|
||||||
|
'max-h-screen py-4',
|
||||||
|
)}
|
||||||
|
fadeSize={100}
|
||||||
|
setMaxSize={false}
|
||||||
|
>
|
||||||
|
<PhotoGridSidebar {...{
|
||||||
|
...categories,
|
||||||
|
photosCount: photosCountWithExcludes,
|
||||||
|
containerHeight,
|
||||||
|
}} />
|
||||||
|
</MaskedScroll>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -50,12 +50,14 @@ export default function PhotoGridSidebar({
|
|||||||
containerHeight,
|
containerHeight,
|
||||||
aboutTextSafelyParsedHtml,
|
aboutTextSafelyParsedHtml,
|
||||||
aboutTextHasBrParagraphBreaks,
|
aboutTextHasBrParagraphBreaks,
|
||||||
|
className,
|
||||||
..._categories
|
..._categories
|
||||||
}: PhotoSetCategories & {
|
}: PhotoSetCategories & {
|
||||||
photosCount: number
|
photosCount: number
|
||||||
containerHeight?: number
|
containerHeight?: number
|
||||||
aboutTextSafelyParsedHtml?: string
|
aboutTextSafelyParsedHtml?: string
|
||||||
aboutTextHasBrParagraphBreaks?: boolean
|
aboutTextHasBrParagraphBreaks?: boolean
|
||||||
|
className?: string
|
||||||
}) {
|
}) {
|
||||||
const categories = useMemo(() => HIDE_TAGS_WITH_ONE_PHOTO
|
const categories = useMemo(() => HIDE_TAGS_WITH_ONE_PHOTO
|
||||||
? {
|
? {
|
||||||
@ -327,7 +329,7 @@ export default function PhotoGridSidebar({
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className={clsx('space-y-4', className)}>
|
||||||
{aboutTextSafelyParsedHtml && <HeaderList
|
{aboutTextSafelyParsedHtml && <HeaderList
|
||||||
items={[<p
|
items={[<p
|
||||||
key="about"
|
key="about"
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { Photo } from '.';
|
|||||||
import { PhotoSetCategory } from '../category';
|
import { PhotoSetCategory } from '../category';
|
||||||
import PhotoGrid from './PhotoGrid';
|
import PhotoGrid from './PhotoGrid';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import { useAppText } from '@/i18n/state/client';
|
||||||
|
|
||||||
export default function PhotoLightbox({
|
export default function PhotoLightbox({
|
||||||
count,
|
count,
|
||||||
@ -16,6 +17,8 @@ export default function PhotoLightbox({
|
|||||||
maxPhotosToShow?: number
|
maxPhotosToShow?: number
|
||||||
moreLink: string
|
moreLink: string
|
||||||
} & PhotoSetCategory) {
|
} & PhotoSetCategory) {
|
||||||
|
const { utility } = useAppText();
|
||||||
|
|
||||||
const photoCountToShow = maxPhotosToShow < count
|
const photoCountToShow = maxPhotosToShow < count
|
||||||
? maxPhotosToShow - 1
|
? maxPhotosToShow - 1
|
||||||
: maxPhotosToShow;
|
: maxPhotosToShow;
|
||||||
@ -44,7 +47,7 @@ export default function PhotoLightbox({
|
|||||||
<div className="text-[1.1rem] lg:text-[1.5rem]">
|
<div className="text-[1.1rem] lg:text-[1.5rem]">
|
||||||
+{countNotShown}
|
+{countNotShown}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-dim">More</div>
|
<div className="text-dim">{utility.more}</div>
|
||||||
</Link>
|
</Link>
|
||||||
: undefined}
|
: undefined}
|
||||||
small
|
small
|
||||||
|
|||||||
@ -167,19 +167,19 @@ export default function PhotoUploadWithStatus({
|
|||||||
{isUploading
|
{isUploading
|
||||||
? isFinishing
|
? isFinishing
|
||||||
? <>
|
? <>
|
||||||
{appText.misc.finishing}
|
{appText.utility.finishing}
|
||||||
</>
|
</>
|
||||||
: <>
|
: <>
|
||||||
{!showButton && uploadStatusText
|
{!showButton && uploadStatusText
|
||||||
? <>
|
? <>
|
||||||
<ResponsiveText shortText={uploadStatusText}>
|
<ResponsiveText shortText={uploadStatusText}>
|
||||||
{appText.misc.uploading} {uploadStatusText}
|
{appText.utility.uploading} {uploadStatusText}
|
||||||
</ResponsiveText>
|
</ResponsiveText>
|
||||||
{': '}
|
{': '}
|
||||||
{fileUploadName}
|
{fileUploadName}
|
||||||
</>
|
</>
|
||||||
: <ResponsiveText shortText={fileUploadName}>
|
: <ResponsiveText shortText={fileUploadName}>
|
||||||
{appText.misc.uploading} {fileUploadName}
|
{appText.utility.uploading} {fileUploadName}
|
||||||
</ResponsiveText>}
|
</ResponsiveText>}
|
||||||
</>
|
</>
|
||||||
: !showButton && <>Initializing</>}
|
: !showButton && <>Initializing</>}
|
||||||
|
|||||||
136
src/photo/TopPhotoEntities.tsx
Normal file
136
src/photo/TopPhotoEntities.tsx
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import PhotoCamera from '@/camera/PhotoCamera';
|
||||||
|
import { PhotoSetCategories } from '@/category';
|
||||||
|
import MaskedScroll from '@/components/MaskedScroll';
|
||||||
|
import PhotoAlbum from '@/album/PhotoAlbum';
|
||||||
|
import PhotoTag from '@/tag/PhotoTag';
|
||||||
|
import PhotoFavs from '@/tag/PhotoFavs';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { CATEGORY_VISIBILITY } from '@/app/config';
|
||||||
|
import PhotoRecents from '@/recents/PhotoRecents';
|
||||||
|
import PhotoFilm from '@/film/PhotoFilm';
|
||||||
|
import PhotoFocalLength from '@/focal/PhotoFocalLength';
|
||||||
|
import PhotoLens from '@/lens/PhotoLens';
|
||||||
|
import PhotoRecipe from '@/recipe/PhotoRecipe';
|
||||||
|
import LoaderButton from '@/components/primitives/LoaderButton';
|
||||||
|
import { useAppState } from '@/app/AppState';
|
||||||
|
import { ComponentProps, useMemo } from 'react';
|
||||||
|
import EntityLink from '@/components/entity/EntityLink';
|
||||||
|
import { useAppText } from '@/i18n/state/client';
|
||||||
|
import { getTopEntities } from '@/category/mobile';
|
||||||
|
import { BiExpandVertical } from 'react-icons/bi';
|
||||||
|
|
||||||
|
const ENTITY_LINK_PROPS: Partial<ComponentProps<typeof EntityLink>> = {
|
||||||
|
badged: true,
|
||||||
|
badgeType: 'medium',
|
||||||
|
truncate: false,
|
||||||
|
suppressSpinner: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function TopPhotoEntities({
|
||||||
|
className,
|
||||||
|
...categories
|
||||||
|
}: PhotoSetCategories & {
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
const { setIsCommandKOpen } = useAppState();
|
||||||
|
|
||||||
|
const { utility } = useAppText();
|
||||||
|
|
||||||
|
const {
|
||||||
|
hasFavs,
|
||||||
|
hasRecents,
|
||||||
|
albums,
|
||||||
|
tags,
|
||||||
|
camera,
|
||||||
|
lens,
|
||||||
|
recipe,
|
||||||
|
film,
|
||||||
|
focal,
|
||||||
|
} = useMemo(() => getTopEntities(categories), [categories]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MaskedScroll
|
||||||
|
direction="horizontal"
|
||||||
|
className={clsx(
|
||||||
|
'flex whitespace-nowrap gap-x-3',
|
||||||
|
// Prevent shadow clipping
|
||||||
|
'py-1',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
fadeSize={50}
|
||||||
|
>
|
||||||
|
{hasFavs &&
|
||||||
|
<PhotoFavs
|
||||||
|
{...ENTITY_LINK_PROPS}
|
||||||
|
badgeIconFirst
|
||||||
|
/>}
|
||||||
|
{hasRecents &&
|
||||||
|
<PhotoRecents
|
||||||
|
key="recents"
|
||||||
|
{...ENTITY_LINK_PROPS}
|
||||||
|
/>}
|
||||||
|
{albums.map(({ album }) =>
|
||||||
|
<PhotoAlbum
|
||||||
|
key={album.id}
|
||||||
|
album={album}
|
||||||
|
{...ENTITY_LINK_PROPS}
|
||||||
|
/>,
|
||||||
|
)}
|
||||||
|
{tags.map(({ tag }) =>
|
||||||
|
<PhotoTag
|
||||||
|
key={tag}
|
||||||
|
tag={tag}
|
||||||
|
{...ENTITY_LINK_PROPS}
|
||||||
|
/>,
|
||||||
|
)}
|
||||||
|
{CATEGORY_VISIBILITY
|
||||||
|
.map(category => {
|
||||||
|
switch (category) {
|
||||||
|
case 'cameras': return camera &&
|
||||||
|
<PhotoCamera
|
||||||
|
key="cameras"
|
||||||
|
camera={camera}
|
||||||
|
{...ENTITY_LINK_PROPS}
|
||||||
|
/>;
|
||||||
|
case 'lenses': return lens &&
|
||||||
|
<PhotoLens
|
||||||
|
key="lenses"
|
||||||
|
lens={lens}
|
||||||
|
{...ENTITY_LINK_PROPS}
|
||||||
|
/>;
|
||||||
|
case 'recipes': return recipe &&
|
||||||
|
<PhotoRecipe
|
||||||
|
key="recipes"
|
||||||
|
recipe={recipe}
|
||||||
|
{...ENTITY_LINK_PROPS}
|
||||||
|
/>;
|
||||||
|
case 'films': return film &&
|
||||||
|
<PhotoFilm
|
||||||
|
key="films"
|
||||||
|
film={film}
|
||||||
|
{...ENTITY_LINK_PROPS}
|
||||||
|
/>;
|
||||||
|
case 'focal-lengths': return focal &&
|
||||||
|
<PhotoFocalLength
|
||||||
|
key="focal-lengths"
|
||||||
|
focal={focal}
|
||||||
|
{...ENTITY_LINK_PROPS}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
<LoaderButton
|
||||||
|
icon={<BiExpandVertical
|
||||||
|
className="text-medium translate-y-[0.75px] text-[0.9rem]"
|
||||||
|
/>}
|
||||||
|
onClick={() => setIsCommandKOpen?.(true)}
|
||||||
|
hideText="never"
|
||||||
|
className={clsx(
|
||||||
|
'h-auto pt-[5px] pb-1.5 pl-1 pr-2.5',
|
||||||
|
'gap-x-[3px] uppercase tracking-wide',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{utility.more}
|
||||||
|
</LoaderButton>
|
||||||
|
</MaskedScroll>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -8,7 +8,10 @@ import EntityLink, {
|
|||||||
} from '@/components/entity/EntityLink';
|
} from '@/components/entity/EntityLink';
|
||||||
import IconFavs from '@/components/icons/IconFavs';
|
import IconFavs from '@/components/icons/IconFavs';
|
||||||
|
|
||||||
export default function PhotoFavs(props: EntityLinkExternalProps) {
|
export default function PhotoFavs({
|
||||||
|
badgeIconFirst,
|
||||||
|
...props
|
||||||
|
}: EntityLinkExternalProps & { badgeIconFirst?: boolean }) {
|
||||||
const { getTagCount } = useCategoryCounts();
|
const { getTagCount } = useCategoryCounts();
|
||||||
return (
|
return (
|
||||||
<EntityLink
|
<EntityLink
|
||||||
@ -21,7 +24,7 @@ export default function PhotoFavs(props: EntityLinkExternalProps) {
|
|||||||
className="translate-x-[-0.5px] translate-y-[-0.5px]"
|
className="translate-x-[-0.5px] translate-y-[-0.5px]"
|
||||||
highlight
|
highlight
|
||||||
/>}
|
/>}
|
||||||
iconBadgeEnd={<IconFavs
|
iconBadgeEnd={!badgeIconFirst && <IconFavs
|
||||||
size={10}
|
size={10}
|
||||||
className="translate-y-[-0.5px]"
|
className="translate-y-[-0.5px]"
|
||||||
highlight
|
highlight
|
||||||
|
|||||||
@ -93,6 +93,10 @@ export const sortTagsWithoutFavs = (tags: string[]) =>
|
|||||||
export const sortTagsObjectWithoutFavs = (tags: Tags) =>
|
export const sortTagsObjectWithoutFavs = (tags: Tags) =>
|
||||||
sortTags(tags, TAG_FAVS);
|
sortTags(tags, TAG_FAVS);
|
||||||
|
|
||||||
|
export const getTopNonFavTags = (tags: Tags) => tags
|
||||||
|
.filter(({ tag }) => tag !== TAG_FAVS)
|
||||||
|
.slice(0, 3);
|
||||||
|
|
||||||
export const descriptionForTaggedPhotos = (
|
export const descriptionForTaggedPhotos = (
|
||||||
photos: Photo[] = [],
|
photos: Photo[] = [],
|
||||||
appText: AppTextState,
|
appText: AppTextState,
|
||||||
@ -189,3 +193,6 @@ export const limitTagsByCount = (
|
|||||||
.toLocaleLowerCase()
|
.toLocaleLowerCase()
|
||||||
.includes(queryToInclude.toLocaleLowerCase()))
|
.includes(queryToInclude.toLocaleLowerCase()))
|
||||||
));
|
));
|
||||||
|
|
||||||
|
export const tagsHaveFavs = (tags: Tags) =>
|
||||||
|
tags.some(({ tag }) => isTagFavs(tag));
|
||||||
|
|||||||
@ -114,6 +114,10 @@ html {
|
|||||||
@apply
|
@apply
|
||||||
text-light dark:text-dark
|
text-light dark:text-dark
|
||||||
}
|
}
|
||||||
|
@utility text-medium-dark {
|
||||||
|
@apply
|
||||||
|
text-gray-600 dark:text-gray-300
|
||||||
|
}
|
||||||
@utility text-medium {
|
@utility text-medium {
|
||||||
@apply
|
@apply
|
||||||
text-gray-500 dark:text-gray-400
|
text-gray-500 dark:text-gray-400
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user