commit
d06c8c3a55
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -3,6 +3,7 @@
|
||||
"ABCDEFGHIJKLMNOP",
|
||||
"Acros",
|
||||
"affordance",
|
||||
"apos",
|
||||
"ARROWLEFT",
|
||||
"ARROWRIGHT",
|
||||
"Astia",
|
||||
|
||||
@ -158,7 +158,7 @@ Application behavior can be changed by configuring the following environment var
|
||||
#### Settings
|
||||
- `NEXT_PUBLIC_GEO_PRIVACY = 1` disables collection/display of location-based data (⚠️ re-compresses uploaded images in order to remove GPS information)
|
||||
- `NEXT_PUBLIC_ALLOW_PUBLIC_DOWNLOADS = 1` enables public photo downloads for all visitors (⚠️ may result in increased bandwidth usage)
|
||||
- `NEXT_PUBLIC_PUBLIC_API = 1` enables public API available at `/api`
|
||||
- `NEXT_PUBLIC_SITE_FEEDS = 1` enables feeds at `/feed.json` and `/rss.xml`
|
||||
- `NEXT_PUBLIC_IGNORE_PRIORITY_ORDER = 1` prevents `priority_order` field affecting photo order
|
||||
- `NEXT_PUBLIC_OG_TEXT_ALIGNMENT = BOTTOM` keeps OG image text bottom aligned (default is top)
|
||||
|
||||
|
||||
@ -1,24 +0,0 @@
|
||||
import { getPhotosCached } from '@/photo/cache';
|
||||
import { API_PHOTO_REQUEST_LIMIT, formatPhotoForApi } from '@/app/api';
|
||||
import {
|
||||
BASE_URL,
|
||||
PUBLIC_API_ENABLED,
|
||||
META_TITLE,
|
||||
} from '@/app/config';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET() {
|
||||
if (PUBLIC_API_ENABLED) {
|
||||
const photos = await getPhotosCached({ limit: API_PHOTO_REQUEST_LIMIT });
|
||||
return Response.json({
|
||||
meta: {
|
||||
title: META_TITLE,
|
||||
url: BASE_URL,
|
||||
},
|
||||
photos: photos.map(formatPhotoForApi),
|
||||
});
|
||||
} else {
|
||||
return new Response('API access disabled', { status: 404 });
|
||||
}
|
||||
}
|
||||
19
app/feed.json/route.ts
Normal file
19
app/feed.json/route.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { getPhotosCached } from '@/photo/cache';
|
||||
import { SITE_FEEDS_ENABLED } from '@/app/config';
|
||||
import { FEED_PHOTO_REQUEST_LIMIT } from '@/feed';
|
||||
import { formatFeedJson } from '@/feed/json';
|
||||
|
||||
// Cache for 24 hours
|
||||
export const revalidate = 86_400;
|
||||
|
||||
export async function GET() {
|
||||
if (SITE_FEEDS_ENABLED) {
|
||||
const photos = await getPhotosCached({
|
||||
limit: FEED_PHOTO_REQUEST_LIMIT,
|
||||
sortBy: 'createdAt',
|
||||
});
|
||||
return Response.json(formatFeedJson(photos));
|
||||
} else {
|
||||
return new Response('Feeds disabled', { status: 404 });
|
||||
}
|
||||
}
|
||||
@ -10,6 +10,7 @@ import {
|
||||
META_TITLE,
|
||||
HTML_LANG,
|
||||
NAV_CAPTION,
|
||||
SITE_FEEDS_ENABLED,
|
||||
} from '@/app/config';
|
||||
import AppStateProvider from '@/state/AppStateProvider';
|
||||
import ToasterWithThemes from '@/toast/ToasterWithThemes';
|
||||
@ -65,6 +66,13 @@ export const metadata: Metadata = {
|
||||
type: 'image/png',
|
||||
sizes: '180x180',
|
||||
}],
|
||||
...SITE_FEEDS_ENABLED && {
|
||||
alternates: {
|
||||
types: {
|
||||
'application/rss+xml': '/rss.xml',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
|
||||
23
app/rss.xml/route.ts
Normal file
23
app/rss.xml/route.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { getPhotosCached } from '@/photo/cache';
|
||||
import { SITE_FEEDS_ENABLED } from '@/app/config';
|
||||
import { FEED_PHOTO_REQUEST_LIMIT } from '@/feed';
|
||||
import { formatFeedRss } from '@/feed/rss';
|
||||
|
||||
// Cache for 24 hours
|
||||
export const revalidate = 86_400;
|
||||
|
||||
export async function GET() {
|
||||
if (SITE_FEEDS_ENABLED) {
|
||||
const photos = await getPhotosCached({
|
||||
limit: FEED_PHOTO_REQUEST_LIMIT,
|
||||
sortBy: 'createdAt',
|
||||
});
|
||||
|
||||
return new Response(
|
||||
formatFeedRss(photos),
|
||||
{ headers: { 'Content-Type': 'text/xml' } },
|
||||
);
|
||||
} else {
|
||||
return new Response('Feeds disabled', { status: 404 });
|
||||
}
|
||||
}
|
||||
@ -31,6 +31,8 @@ import ScoreCardContainer from '@/components/ScoreCardContainer';
|
||||
import { DEFAULT_CATEGORY_KEYS, getHiddenCategories } from '@/category';
|
||||
import { AI_AUTO_GENERATED_FIELDS_ALL } from '@/photo/ai';
|
||||
import clsx from 'clsx/lite';
|
||||
import Link from 'next/link';
|
||||
import { PATH_FEED_JSON, PATH_RSS_XML } from '@/app/paths';
|
||||
|
||||
export default function AdminAppConfigurationClient({
|
||||
// Storage
|
||||
@ -103,7 +105,7 @@ export default function AdminAppConfigurationClient({
|
||||
// Settings
|
||||
isGeoPrivacyEnabled,
|
||||
arePublicDownloadsEnabled,
|
||||
isPublicApiEnabled,
|
||||
areSiteFeedsEnabled,
|
||||
isPriorityOrderEnabled,
|
||||
isOgTextBottomAligned,
|
||||
// Internal
|
||||
@ -177,6 +179,15 @@ export default function AdminAppConfigurationClient({
|
||||
{message}
|
||||
</ErrorNote>;
|
||||
|
||||
const renderLink = (href: string, children?: ReactNode) =>
|
||||
<Link
|
||||
href={href}
|
||||
className="underline underline-offset-3 hover:no-underline"
|
||||
target="_blank"
|
||||
>
|
||||
{children || href}
|
||||
</Link>;
|
||||
|
||||
return (
|
||||
<ScoreCardContainer>
|
||||
<ChecklistGroup
|
||||
@ -734,13 +745,14 @@ export default function AdminAppConfigurationClient({
|
||||
{renderEnvVars(['NEXT_PUBLIC_ALLOW_PUBLIC_DOWNLOADS'])}
|
||||
</ChecklistRow>
|
||||
<ChecklistRow
|
||||
title="Public API"
|
||||
status={isPublicApiEnabled}
|
||||
title="Site feeds (JSON/RSS)"
|
||||
status={areSiteFeedsEnabled}
|
||||
optional
|
||||
>
|
||||
Set environment variable to {'"1"'} to enable
|
||||
a public API available at <code>/api</code>:
|
||||
{renderEnvVars(['NEXT_PUBLIC_PUBLIC_API'])}
|
||||
Set environment variable to {'"1"'} to enable feeds at
|
||||
{' '}
|
||||
{renderLink(PATH_FEED_JSON)} and {renderLink(PATH_RSS_XML)}:
|
||||
{renderEnvVars(['NEXT_PUBLIC_SITE_FEEDS'])}
|
||||
</ChecklistRow>
|
||||
<ChecklistRow
|
||||
title="Priority order"
|
||||
|
||||
@ -1,43 +0,0 @@
|
||||
import { Photo } from '@/photo';
|
||||
import { absolutePathForPhoto } from './paths';
|
||||
import { formatDateFromPostgresString } from '@/utility/date';
|
||||
import { getNextImageUrlForRequest } from '@/platforms/next-image';
|
||||
|
||||
export const API_PHOTO_REQUEST_LIMIT = 40;
|
||||
|
||||
export interface PublicApi {
|
||||
meta: {
|
||||
title: string
|
||||
url: string
|
||||
}
|
||||
photos: PublicApiPhoto[]
|
||||
}
|
||||
|
||||
interface PublicApiPhoto {
|
||||
id: string
|
||||
title?: string
|
||||
url: string
|
||||
make?: string
|
||||
model?: string
|
||||
tags?: string[]
|
||||
takenAtNaive: string
|
||||
src: Record<
|
||||
'small' | 'medium' | 'large',
|
||||
string
|
||||
>
|
||||
}
|
||||
|
||||
export const formatPhotoForApi = (photo: Photo): PublicApiPhoto => ({
|
||||
id: photo.id,
|
||||
title: photo.title,
|
||||
url: absolutePathForPhoto({ photo }),
|
||||
...photo.make && { make: photo.make },
|
||||
...photo.model && { model: photo.model },
|
||||
...photo.tags.length > 0 && { tags: photo.tags },
|
||||
takenAtNaive: formatDateFromPostgresString(photo.takenAtNaive),
|
||||
src: {
|
||||
small: getNextImageUrlForRequest({ imageUrl: photo.url, size: 200 }),
|
||||
medium: getNextImageUrlForRequest({ imageUrl: photo.url, size: 640 }),
|
||||
large: getNextImageUrlForRequest({ imageUrl: photo.url, size: 1200 }),
|
||||
},
|
||||
});
|
||||
@ -307,8 +307,8 @@ export const GEO_PRIVACY_ENABLED =
|
||||
process.env.NEXT_PUBLIC_GEO_PRIVACY === '1';
|
||||
export const ALLOW_PUBLIC_DOWNLOADS =
|
||||
process.env.NEXT_PUBLIC_ALLOW_PUBLIC_DOWNLOADS === '1';
|
||||
export const PUBLIC_API_ENABLED =
|
||||
process.env.NEXT_PUBLIC_PUBLIC_API === '1';
|
||||
export const SITE_FEEDS_ENABLED =
|
||||
process.env.NEXT_PUBLIC_SITE_FEEDS === '1';
|
||||
export const PRIORITY_ORDER_ENABLED =
|
||||
process.env.NEXT_PUBLIC_IGNORE_PRIORITY_ORDER !== '1';
|
||||
export const OG_TEXT_BOTTOM_ALIGNMENT =
|
||||
@ -417,7 +417,7 @@ export const APP_CONFIGURATION = {
|
||||
// Settings
|
||||
isGeoPrivacyEnabled: GEO_PRIVACY_ENABLED,
|
||||
arePublicDownloadsEnabled: ALLOW_PUBLIC_DOWNLOADS,
|
||||
isPublicApiEnabled: PUBLIC_API_ENABLED,
|
||||
areSiteFeedsEnabled: SITE_FEEDS_ENABLED,
|
||||
isPriorityOrderEnabled: PRIORITY_ORDER_ENABLED,
|
||||
isOgTextBottomAligned: OG_TEXT_BOTTOM_ALIGNMENT,
|
||||
// Internal
|
||||
|
||||
@ -15,6 +15,10 @@ export const PATH_API = '/api';
|
||||
export const PATH_SIGN_IN = '/sign-in';
|
||||
export const PATH_OG = '/og';
|
||||
|
||||
// Feeds
|
||||
export const PATH_FEED_JSON = '/feed.json';
|
||||
export const PATH_RSS_XML = '/rss.xml';
|
||||
|
||||
export const PATH_GRID_INFERRED = GRID_HOMEPAGE_ENABLED
|
||||
? PATH_ROOT
|
||||
: PATH_GRID;
|
||||
@ -167,6 +171,12 @@ export const pathForRecipe = (recipe: string) =>
|
||||
`${PREFIX_RECIPE}/${recipe}`;
|
||||
|
||||
// Absolute paths
|
||||
export const ABSOLUTE_PATH_FOR_FEED_JSON =
|
||||
`${getBaseUrl()}${PATH_FEED_JSON}`;
|
||||
|
||||
export const ABSOLUTE_PATH_FOR_RSS_XML =
|
||||
`${getBaseUrl()}${PATH_RSS_XML}`;
|
||||
|
||||
export const ABSOLUTE_PATH_FOR_HOME_IMAGE =
|
||||
`${getBaseUrl()}/home-image`;
|
||||
|
||||
|
||||
32
src/feed/index.ts
Normal file
32
src/feed/index.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { descriptionForPhoto, Photo, titleForPhoto } from '@/photo';
|
||||
import {
|
||||
getNextImageUrlForRequest,
|
||||
NextImageSize,
|
||||
} from '@/platforms/next-image';
|
||||
|
||||
export const FEED_PHOTO_REQUEST_LIMIT = 40;
|
||||
|
||||
export const FEED_PHOTO_WIDTH_SMALL = 200;
|
||||
export const FEED_PHOTO_WIDTH_MEDIUM = 640;
|
||||
export const FEED_PHOTO_WIDTH_LARGE = 1200;
|
||||
|
||||
export interface FeedMedia {
|
||||
url: string
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
export const generateFeedMedia = (
|
||||
photo: Photo,
|
||||
size: NextImageSize,
|
||||
): FeedMedia => ({
|
||||
url: getNextImageUrlForRequest({ imageUrl: photo.url, size }),
|
||||
width: size,
|
||||
height: Math.round(size / photo.aspectRatio),
|
||||
});
|
||||
|
||||
export const getCoreFeedFields = (photo: Photo) => ({
|
||||
id: photo.id,
|
||||
title: titleForPhoto(photo),
|
||||
description: descriptionForPhoto(photo, true),
|
||||
});
|
||||
46
src/feed/json.ts
Normal file
46
src/feed/json.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { absolutePathForPhoto } from '@/app/paths';
|
||||
import {
|
||||
FEED_PHOTO_WIDTH_LARGE,
|
||||
FEED_PHOTO_WIDTH_MEDIUM,
|
||||
FEED_PHOTO_WIDTH_SMALL,
|
||||
FeedMedia,
|
||||
generateFeedMedia,
|
||||
getCoreFeedFields,
|
||||
} from '.';
|
||||
import { formatDateFromPostgresString } from '@/utility/date';
|
||||
import { Photo } from '@/photo';
|
||||
import { BASE_URL } from '@/app/config';
|
||||
import { META_TITLE } from '@/app/config';
|
||||
|
||||
interface FeedPhotoJson {
|
||||
id: string
|
||||
title: string
|
||||
url: string
|
||||
make?: string
|
||||
model?: string
|
||||
tags?: string[]
|
||||
takenAtNaive: string
|
||||
src: Record<'small' | 'medium' | 'large', FeedMedia>
|
||||
}
|
||||
|
||||
const formatPhotoForFeedJson = (photo: Photo): FeedPhotoJson => ({
|
||||
...getCoreFeedFields(photo),
|
||||
url: absolutePathForPhoto({ photo }),
|
||||
...photo.make && { make: photo.make },
|
||||
...photo.model && { model: photo.model },
|
||||
...photo.tags.length > 0 && { tags: photo.tags },
|
||||
takenAtNaive: formatDateFromPostgresString(photo.takenAtNaive),
|
||||
src: {
|
||||
small: generateFeedMedia(photo, FEED_PHOTO_WIDTH_SMALL),
|
||||
medium: generateFeedMedia(photo, FEED_PHOTO_WIDTH_MEDIUM),
|
||||
large: generateFeedMedia(photo, FEED_PHOTO_WIDTH_LARGE),
|
||||
},
|
||||
});
|
||||
|
||||
export const formatFeedJson = (photos: Photo[]) => ({
|
||||
meta: {
|
||||
title: META_TITLE,
|
||||
url: BASE_URL,
|
||||
},
|
||||
photos: photos.map(formatPhotoForFeedJson),
|
||||
});
|
||||
78
src/feed/rss.ts
Normal file
78
src/feed/rss.ts
Normal file
@ -0,0 +1,78 @@
|
||||
import { Photo } from '@/photo';
|
||||
import {
|
||||
FEED_PHOTO_WIDTH_LARGE,
|
||||
FEED_PHOTO_WIDTH_MEDIUM,
|
||||
FeedMedia,
|
||||
generateFeedMedia,
|
||||
getCoreFeedFields,
|
||||
} from '.';
|
||||
import { ABSOLUTE_PATH_FOR_RSS_XML, absolutePathForPhoto } from '@/app/paths';
|
||||
import { formatDate } from '@/utility/date';
|
||||
import { formatStringForXml } from '@/utility/string';
|
||||
import { BASE_URL, META_DESCRIPTION, META_TITLE } from '@/app/config';
|
||||
|
||||
interface FeedPhotoRss {
|
||||
id: string
|
||||
title: string
|
||||
description?: string
|
||||
link: string
|
||||
pubDate: Date
|
||||
media: Record<'content' | 'thumb', FeedMedia>
|
||||
}
|
||||
|
||||
const formatPhotoForFeedRss = (photo: Photo): FeedPhotoRss => ({
|
||||
...getCoreFeedFields(photo),
|
||||
link: absolutePathForPhoto({ photo }),
|
||||
pubDate: photo.createdAt,
|
||||
media: {
|
||||
content: generateFeedMedia(photo, FEED_PHOTO_WIDTH_LARGE),
|
||||
thumb: generateFeedMedia(photo, FEED_PHOTO_WIDTH_MEDIUM),
|
||||
},
|
||||
});
|
||||
|
||||
const feedPhotoToXml = (photo: FeedPhotoRss): string => {
|
||||
return `<item>
|
||||
<title>${photo.title}</title>
|
||||
<link>${photo.link}</link>
|
||||
<pubDate>
|
||||
${formatDate({ date: photo.pubDate, length: 'rss' })}
|
||||
</pubDate>
|
||||
<guid isPermaLink="true">${photo.link}</guid>
|
||||
${photo.description
|
||||
? `<description><![CDATA[${photo.description}]]></description>`
|
||||
: ''}
|
||||
<media:content
|
||||
url="${formatStringForXml(photo.media.content.url)}"
|
||||
type="image/jpeg"
|
||||
medium="image"
|
||||
width="${photo.media.content.width}"
|
||||
height="${photo.media.content.height}"
|
||||
>
|
||||
<media:thumbnail
|
||||
url="${formatStringForXml(photo.media.thumb.url)}"
|
||||
width="${photo.media.thumb.width}"
|
||||
height="${photo.media.thumb.height}"
|
||||
/>
|
||||
</media:content>
|
||||
</item>`;
|
||||
};
|
||||
|
||||
export const formatFeedRss = (photos: Photo[]) =>
|
||||
`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0"
|
||||
xmlns:content="http://purl.org/rss/1.0/modules/content/"
|
||||
xmlns:atom="http://www.w3.org/2005/Atom"
|
||||
xmlns:media="http://search.yahoo.com/mrss/"
|
||||
>
|
||||
<channel>
|
||||
<title>${META_TITLE}</title>
|
||||
<atom:link
|
||||
href="${ABSOLUTE_PATH_FOR_RSS_XML}"
|
||||
rel="self"
|
||||
type="application/rss+xml"
|
||||
/>
|
||||
<link>${BASE_URL}</link>
|
||||
<description>${META_DESCRIPTION}</description>
|
||||
${photos.map(formatPhotoForFeedRss).map(feedPhotoToXml).join('\n')}
|
||||
</channel>
|
||||
</rss>`;
|
||||
@ -5,26 +5,29 @@ import { setDefaultDateFnLocale } from '@/i18n';
|
||||
|
||||
setDefaultDateFnLocale();
|
||||
|
||||
const DATE_STRING_FORMAT_TINY = 'dd MMM yy';
|
||||
const DATE_STRING_FORMAT_TINY_PLACEHOLDER = '00 000 00';
|
||||
const DATE_FORMAT_TINY = 'dd MMM yy';
|
||||
const DATE_FORMAT_TINY_PLACEHOLDER = '00 000 00';
|
||||
|
||||
const DATE_STRING_FORMAT_SHORT = 'dd MMM yyyy';
|
||||
const DATE_STRING_FORMAT_SHORT_PLACEHOLDER = '00 000 0000';
|
||||
const DATE_FORMAT_SHORT = 'dd MMM yyyy';
|
||||
const DATE_FORMAT_SHORT_PLACEHOLDER = '00 000 0000';
|
||||
|
||||
const DATE_STRING_FORMAT_MEDIUM = 'dd MMM yy h:mma';
|
||||
const DATE_STRING_FORMAT_MEDIUM_PLACEHOLDER = '00 000 00 00:0000';
|
||||
const DATE_FORMAT_MEDIUM = 'dd MMM yy h:mma';
|
||||
const DATE_FORMAT_MEDIUM_PLACEHOLDER = '00 000 00 00:0000';
|
||||
|
||||
const DATE_STRING_FORMAT_LONG = 'dd MMM yyyy h:mma';
|
||||
const DATE_STRING_FORMAT_LONG_PLACEHOLDER = '00 000 0000 00:0000';
|
||||
const DATE_FORMAT_LONG = 'dd MMM yyyy h:mma';
|
||||
const DATE_FORMAT_LONG_PLACEHOLDER = '00 000 0000 00:0000';
|
||||
|
||||
const DATE_STRING_FORMAT_POSTGRES = 'yyyy-MM-dd HH:mm:ss';
|
||||
const DATE_FORMAT_RSS = 'EEE, dd LLL yyyy HH:mm:ss xx';
|
||||
const DATE_FORMAT_RSS_PLACEHOLDER = '000, 00 000 0000 00:00:00 00';
|
||||
|
||||
const DATE_FORMAT_POSTGRES = 'yyyy-MM-dd HH:mm:ss';
|
||||
|
||||
export const VALIDATION_EXAMPLE_POSTGRES = '2025-01-03T21:00:44.000Z';
|
||||
export const VALIDATION_EXAMPLE_POSTGRES_NAIVE = '2025-01-03 16:00:44';
|
||||
|
||||
type AmbiguousTimestamp = number | string;
|
||||
|
||||
type Length = 'tiny' | 'short' | 'medium' | 'long';
|
||||
type Length = 'tiny' | 'short' | 'medium' | 'long' | 'rss';
|
||||
|
||||
export const formatDate = ({
|
||||
date,
|
||||
@ -40,28 +43,32 @@ export const formatDate = ({
|
||||
showPlaceholder?: boolean,
|
||||
}) => {
|
||||
let formatString = !hideTime
|
||||
? DATE_STRING_FORMAT_LONG
|
||||
: DATE_STRING_FORMAT_SHORT;
|
||||
? DATE_FORMAT_LONG
|
||||
: DATE_FORMAT_SHORT;
|
||||
let placeholderString = !hideTime
|
||||
? DATE_STRING_FORMAT_LONG_PLACEHOLDER
|
||||
: DATE_STRING_FORMAT_SHORT_PLACEHOLDER;
|
||||
? DATE_FORMAT_LONG_PLACEHOLDER
|
||||
: DATE_FORMAT_SHORT_PLACEHOLDER;
|
||||
|
||||
switch (length) {
|
||||
case 'rss':
|
||||
formatString = DATE_FORMAT_RSS;
|
||||
placeholderString = DATE_FORMAT_RSS_PLACEHOLDER;
|
||||
break;
|
||||
case 'tiny':
|
||||
formatString = DATE_STRING_FORMAT_TINY;
|
||||
placeholderString = DATE_STRING_FORMAT_TINY_PLACEHOLDER;
|
||||
formatString = DATE_FORMAT_TINY;
|
||||
placeholderString = DATE_FORMAT_TINY_PLACEHOLDER;
|
||||
break;
|
||||
case 'short':
|
||||
formatString = DATE_STRING_FORMAT_SHORT;
|
||||
placeholderString = DATE_STRING_FORMAT_SHORT_PLACEHOLDER;
|
||||
formatString = DATE_FORMAT_SHORT;
|
||||
placeholderString = DATE_FORMAT_SHORT_PLACEHOLDER;
|
||||
break;
|
||||
case 'medium':
|
||||
formatString = !hideTime
|
||||
? DATE_STRING_FORMAT_MEDIUM
|
||||
: DATE_STRING_FORMAT_SHORT;
|
||||
? DATE_FORMAT_MEDIUM
|
||||
: DATE_FORMAT_SHORT;
|
||||
placeholderString = !hideTime
|
||||
? DATE_STRING_FORMAT_MEDIUM_PLACEHOLDER
|
||||
: DATE_STRING_FORMAT_SHORT_PLACEHOLDER;
|
||||
? DATE_FORMAT_MEDIUM_PLACEHOLDER
|
||||
: DATE_FORMAT_SHORT_PLACEHOLDER;
|
||||
break;
|
||||
}
|
||||
|
||||
@ -77,7 +84,7 @@ export const formatDateFromPostgresString = (
|
||||
length?: Length,
|
||||
) =>
|
||||
formatDate({
|
||||
date: parse(date, DATE_STRING_FORMAT_POSTGRES, new Date()),
|
||||
date: parse(date, DATE_FORMAT_POSTGRES, new Date()),
|
||||
length,
|
||||
});
|
||||
|
||||
@ -130,7 +137,7 @@ export const generateLocalPostgresString = () =>
|
||||
formatDateForPostgres(new Date());
|
||||
|
||||
export const generateLocalNaivePostgresString = () =>
|
||||
format(new Date(), DATE_STRING_FORMAT_POSTGRES);
|
||||
format(new Date(), DATE_FORMAT_POSTGRES);
|
||||
|
||||
// Form validation to prevent Postgres runtime errors
|
||||
|
||||
|
||||
@ -35,6 +35,14 @@ export const parameterize = (
|
||||
)
|
||||
.toLocaleLowerCase();
|
||||
|
||||
export const formatStringForXml = (string: string) =>
|
||||
string
|
||||
.replace(/&/g, '&')
|
||||
.replace(/'/g, ''')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
|
||||
export const deparameterize = (string: string) =>
|
||||
capitalizeWords(string.replaceAll('-', ' '));
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user