diff --git a/README.md b/README.md index bbd07dd1..1544adde 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,7 @@ Application behavior can be changed by configuring the following environment var - `NEXT_PUBLIC_IGNORE_PRIORITY_ORDER = 1` prevents `priority_order` field affecting photo order - `NEXT_PUBLIC_PUBLIC_API = 1` enables public API available at `/api` - `NEXT_PUBLIC_HIDE_REPO_LINK = 1` removes footer link to repo +- `NEXT_PUBLIC_HIDE_SOCIAL = 1` removes X button from share modal - `NEXT_PUBLIC_HIDE_FILM_SIMULATIONS = 1` prevents Fujifilm simulations showing up in `/grid` sidebar and CMD-K search results - `NEXT_PUBLIC_HIDE_EXIF_DATA = 1` hides EXIF data in photo details and OG images (potentially useful for portfolios, which don't focus on photography) - `NEXT_PUBLIC_GRID_ASPECT_RATIO = 1.5` sets aspect ratio for grid tiles (defaults to `1`—setting to `0` removes the constraint) diff --git a/__tests__/number.test.ts b/__tests__/number.test.ts new file mode 100644 index 00000000..1d19a108 --- /dev/null +++ b/__tests__/number.test.ts @@ -0,0 +1,24 @@ +import { roundToString, roundToNumber } from '@/utility/number'; + +describe('number', () => { + describe('rounds to a', () => { + it('string', () => { + expect(roundToString(1.2345, 1)).toBe('1.2'); + expect(roundToString(1.2345, 2)).toBe('1.23'); + expect(roundToString(1.2355, 2)).toBe('1.24'); + expect(roundToString(1.2355, 3)).toBe('1.236'); + expect(roundToString(1.78, 1)).toBe('1.8'); + expect(roundToString(1.0, 1, false)).toBe('1'); + expect(roundToString(1.0, 1, true)).toBe('1.0'); + }); + it('number', () => { + expect(roundToNumber(1.2345, 1)).toBe(1.2); + expect(roundToNumber(1.2345, 2)).toBe(1.23); + expect(roundToNumber(1.2355, 2)).toBe(1.24); + expect(roundToNumber(1.2355, 3)).toBe(1.236); + expect(roundToNumber(1.78, 1)).toBe(1.8); + expect(roundToNumber(1.0, 1, false)).toBe(1); + expect(roundToNumber(1.0, 1, true)).toBe(1); + }); + }); +}); \ No newline at end of file diff --git a/__tests__/path.test.ts b/__tests__/path.test.ts index 5bbe5016..be19437a 100644 --- a/__tests__/path.test.ts +++ b/__tests__/path.test.ts @@ -37,6 +37,9 @@ const SHARE = 'share'; const PATH_ROOT = '/'; const PATH_GRID = '/grid'; const PATH_ADMIN = '/admin/photos'; +const PATH_OG = '/og'; +const PATH_OG_ALL = `${PATH_OG}/all`; +const PATH_OG_SAMPLE = `${PATH_OG}/sample`; const PATH_PHOTO = `/p/${PHOTO_ID}`; const PATH_PHOTO_SHARE = `${PATH_PHOTO}/${SHARE}`; @@ -77,6 +80,9 @@ describe('Paths', () => { expect(isPathProtected(PATH_FILM_SIMULATION)).toBe(false); // Private expect(isPathProtected(PATH_ADMIN)).toBe(true); + expect(isPathProtected(PATH_OG)).toBe(true); + expect(isPathProtected(PATH_OG_ALL)).toBe(true); + expect(isPathProtected(PATH_OG_SAMPLE)).toBe(true); expect(isPathProtected(PATH_TAG_HIDDEN)).toBe(true); expect(isPathProtected(PATH_TAG_HIDDEN_SHARE)).toBe(true); expect(isPathProtected(PATH_TAG_HIDDEN_PHOTO)).toBe(true); diff --git a/src/app/og/page.tsx b/src/app/og/all/page.tsx similarity index 95% rename from src/app/og/page.tsx rename to src/app/og/all/page.tsx index 1394c43f..27c1523d 100644 --- a/src/app/og/page.tsx +++ b/src/app/og/all/page.tsx @@ -7,7 +7,7 @@ import { getPhotosMeta } from '@/photo/db/query'; import StaggeredOgPhotos from '@/photo/StaggeredOgPhotos'; import StaggeredOgPhotosInfinite from '@/photo/StaggeredOgPhotosInfinite'; -export default async function GridPage() { +export default async function OGPage() { const [ photos, count, diff --git a/src/app/og/sample/page.tsx b/src/app/og/sample/page.tsx new file mode 100644 index 00000000..5f23654d --- /dev/null +++ b/src/app/og/sample/page.tsx @@ -0,0 +1,47 @@ +import CameraOGTile from '@/camera/CameraOGTile'; +import FocalLengthOGTile from '@/focal/FocalLengthOGTile'; +import PhotoOGTile from '@/photo/PhotoOGTile'; +import { getPhotosCached } from '@/photo/cache'; +import FilmSimulationOGTile from '@/simulation/FilmSimulationOGTile'; +import { TAG_FAVS } from '@/tag'; +import TagOGTile from '@/tag/TagOGTile'; + +const tag = 'cicadas'; +const camera = { make: 'Fujifilm', model: 'X-T5' }; +const cameraIcon = { make: 'Apple', model: 'iPhone 13 Pro' }; +const simulation = 'acros'; +const focal = 90; + +export default async function OGOverviewPage() { + const [ + photosBasic, + photosIcon, + photosTag, + photosFavs, + photosCamera, + photosSimulation, + photosFocal, + ] = await Promise.all([ + getPhotosCached({ limit: 1 }), + getPhotosCached({ limit: 1, camera: cameraIcon }), + getPhotosCached({ limit: 1, tag }), + getPhotosCached({ limit: 1, tag: TAG_FAVS }), + getPhotosCached({ limit: 1, camera }), + getPhotosCached({ limit: 1, simulation }), + getPhotosCached({ limit: 1, focal }), + ]); + + console.log(photosIcon); + + return ( +
+ + + + + + + +
+ ); +} diff --git a/src/camera/CameraShareModal.tsx b/src/camera/CameraShareModal.tsx index 0bdb0884..3eafbe47 100644 --- a/src/camera/CameraShareModal.tsx +++ b/src/camera/CameraShareModal.tsx @@ -3,6 +3,7 @@ import { Photo, PhotoDateRange } from '../photo'; import ShareModal from '@/components/ShareModal'; import CameraOGTile from './CameraOGTile'; import { Camera } from '.'; +import { shareTextForCamera } from './meta'; export default function CameraShareModal({ camera, @@ -17,9 +18,9 @@ export default function CameraShareModal({ }) { return ( diff --git a/src/camera/meta.ts b/src/camera/meta.ts index 35c0c2ca..ff13db16 100644 --- a/src/camera/meta.ts +++ b/src/camera/meta.ts @@ -24,6 +24,15 @@ export const titleForCamera = ( photoQuantityText(explicitCount ?? photos.length), ].join(' '); +export const shareTextForCamera = ( + camera: Camera, + photos: Photo[], +) => + [ + 'Photos shot on', + formatCameraText(cameraFromPhoto(photos[0], camera)), + ].join(' '); + export const descriptionForCameraPhotos = ( photos: Photo[], dateBased?: boolean, diff --git a/src/components/OGTile.tsx b/src/components/OGTile.tsx index e0d19a0b..9c9dd4cd 100644 --- a/src/components/OGTile.tsx +++ b/src/components/OGTile.tsx @@ -118,8 +118,8 @@ export default function OGTile({ />}
void, + embedded?: boolean, + ) => +
+ {icon} +
; + return (
-
- -
- {title} -
-
+ {title && +
+ +
+ {title} +
+
} {children} -
-
- {shortenUrl(pathShare)} -
-
+
+
+ {shortenUrl(pathShare)} +
+ {renderIcon( + , + () => { + navigator.clipboard.writeText(pathShare); + toastSuccess('Link to photo copied'); + }, + true, )} - onClick={() => { - navigator.clipboard.writeText(pathShare); - toastSuccess('Link to photo copied'); - }} - > -
+ {SHOW_SOCIAL && + renderIcon( + , + () => window.open( + generateXPostText(pathShare, socialText), + '_blank', + ), + )}
diff --git a/src/focal/FocalLengthShareModal.tsx b/src/focal/FocalLengthShareModal.tsx index 62b02b41..6b421c0f 100644 --- a/src/focal/FocalLengthShareModal.tsx +++ b/src/focal/FocalLengthShareModal.tsx @@ -2,6 +2,7 @@ import { absolutePathForFocalLength, pathForFocalLength } from '@/site/paths'; import { Photo, PhotoDateRange } from '../photo'; import ShareModal from '@/components/ShareModal'; import FocalLengthOGTile from './FocalLengthOGTile'; +import { shareTextFocalLength } from '.'; export default function FocalLengthShareModal({ focal, @@ -16,9 +17,9 @@ export default function FocalLengthShareModal({ }) { return ( diff --git a/src/focal/index.ts b/src/focal/index.ts index f3f40bd3..150e3317 100644 --- a/src/focal/index.ts +++ b/src/focal/index.ts @@ -27,6 +27,9 @@ export const titleForFocalLength = ( photoQuantityText(explicitCount ?? photos.length), ].join(' '); +export const shareTextFocalLength = (focal: number) => + `Photos shot at ${formatFocalLength(focal)}`; + export const descriptionForFocalLengthPhotos = ( photos: Photo[], dateBased?: boolean, diff --git a/src/image-response/CameraImageResponse.tsx b/src/image-response/CameraImageResponse.tsx index 58eb316b..e698bd0a 100644 --- a/src/image-response/CameraImageResponse.tsx +++ b/src/image-response/CameraImageResponse.tsx @@ -33,14 +33,19 @@ export default function CameraImageResponse({ height, }} /> - - - - {formatCameraText(camera)} - + , + }}> + {formatCameraText(camera).toLocaleUpperCase()} ); diff --git a/src/image-response/FilmSimulationImageResponse.tsx b/src/image-response/FilmSimulationImageResponse.tsx index 5c5c630c..66112674 100644 --- a/src/image-response/FilmSimulationImageResponse.tsx +++ b/src/image-response/FilmSimulationImageResponse.tsx @@ -36,15 +36,17 @@ export default function FilmSimulationImageResponse({ height, }} /> - - - - {labelForFilmSimulation(simulation).medium} - + height={height * .081} + style={{ transform: `translateY(${height * .001}px)`}} + />, + }}> + {labelForFilmSimulation(simulation).medium.toLocaleUpperCase()} ); diff --git a/src/image-response/FocalLengthImageResponse.tsx b/src/image-response/FocalLengthImageResponse.tsx index 498f9cfd..d2435272 100644 --- a/src/image-response/FocalLengthImageResponse.tsx +++ b/src/image-response/FocalLengthImageResponse.tsx @@ -32,14 +32,19 @@ export default function FocalLengthImageResponse({ height, }} /> - - - {formatFocalLength(focal)} + />, + }}> + {formatFocalLength(focal)} ); diff --git a/src/image-response/PhotoImageResponse.tsx b/src/image-response/PhotoImageResponse.tsx index 2175add2..943188a9 100644 --- a/src/image-response/PhotoImageResponse.tsx +++ b/src/image-response/PhotoImageResponse.tsx @@ -20,9 +20,16 @@ export default function PhotoImageResponse({ fontFamily: string isNextImageReady: boolean }) { - const model = photo.model - ? formatCameraModelTextShort(cameraFromPhoto(photo)) - : undefined; + const caption = [ + photo.model + ? formatCameraModelTextShort(cameraFromPhoto(photo)) + : undefined, + photo.focalLengthFormatted, + photo.fNumberFormatted, + photo.isoFormatted, + ] + .join(' ') + .trim(); return ( @@ -33,24 +40,15 @@ export default function PhotoImageResponse({ ...OG_TEXT_BOTTOM_ALIGNMENT && { imagePosition: 'top' }, }} /> {shouldShowExifDataForPhoto(photo) && - - {photo.make === 'Apple' && -
- -
} - {model && -
- {model} -
} -
- {photo.focalLengthFormatted} -
-
- {photo.fNumberFormatted} -
-
- {photo.isoFormatted} -
+ }, + }}> + {caption} }
); diff --git a/src/image-response/TagImageResponse.tsx b/src/image-response/TagImageResponse.tsx index d7a7f0e5..d5f11817 100644 --- a/src/image-response/TagImageResponse.tsx +++ b/src/image-response/TagImageResponse.tsx @@ -32,21 +32,29 @@ export default function TagImageResponse({ height, }} /> - - {isTagFavs(tag) + : } - {tag.toUpperCase()} + size={height * .06} + style={{ + transform: `translateY(${height * .016}px)`, + marginRight: height * .015, + }} + />, + }}> + {tag.toLocaleUpperCase()} ); diff --git a/src/image-response/components/ImageCaption.tsx b/src/image-response/components/ImageCaption.tsx index 75c1f9f6..498136c3 100644 --- a/src/image-response/components/ImageCaption.tsx +++ b/src/image-response/components/ImageCaption.tsx @@ -6,59 +6,50 @@ const GRADIENT_STOPS = 'rgba(0,0,0,0), rgba(0,0,0,0.3), rgba(0,0,0,0.7)'; export default function ImageCaption({ height, fontFamily, - subhead, + icon, children, }: { width: number height: number fontFamily: string - subhead?: ReactNode + icon?: ReactNode children: ReactNode }) { + const paddingEdge = height * .07; + const paddingContent = height * .6; return (
- {subhead && -
- {subhead} -
} + {icon}
diff --git a/src/photo/StaggeredOgPhotos.tsx b/src/photo/StaggeredOgPhotos.tsx index cf00b18d..fa9fd95d 100644 --- a/src/photo/StaggeredOgPhotos.tsx +++ b/src/photo/StaggeredOgPhotos.tsx @@ -65,7 +65,7 @@ export default function StaggeredOgPhotos({ onFail={() => recomputeLoadingState({ [photo.id]: 'failed' })} onVisible={index === photos.length - 1 ? onLastPhotoVisible - :undefined} + : undefined} riseOnHover />)}
diff --git a/src/photo/form/index.ts b/src/photo/form/index.ts index e6df9f75..32db49bd 100644 --- a/src/photo/form/index.ts +++ b/src/photo/form/index.ts @@ -7,7 +7,7 @@ import { generateLocalPostgresString, } from '@/utility/date'; import { getAspectRatioFromExif, getOffsetFromExif } from '@/utility/exif'; -import { toFixedNumber } from '@/utility/number'; +import { roundToNumber } from '@/utility/number'; import { convertStringToArray } from '@/utility/string'; import { generateNanoid } from '@/utility/nanoid'; import { @@ -251,7 +251,7 @@ export const convertFormDataToPhotoDbInsert = ( // Convert form strings to arrays tags: tags.length > 0 ? tags : undefined, // Convert form strings to numbers - aspectRatio: toFixedNumber(parseFloat(photoForm.aspectRatio), 6), + aspectRatio: roundToNumber(parseFloat(photoForm.aspectRatio), 6), focalLength: photoForm.focalLength ? parseInt(photoForm.focalLength) : undefined, diff --git a/src/simulation/FilmSimulationShareModal.tsx b/src/simulation/FilmSimulationShareModal.tsx index 3369b82e..c0298c4b 100644 --- a/src/simulation/FilmSimulationShareModal.tsx +++ b/src/simulation/FilmSimulationShareModal.tsx @@ -5,7 +5,7 @@ import { import { Photo, PhotoDateRange } from '../photo'; import ShareModal from '@/components/ShareModal'; import FilmSimulationOGTile from './FilmSimulationOGTile'; -import { FilmSimulation } from '.'; +import { FilmSimulation, shareTextForFilmSimulation } from '.'; export default function FilmSimulationShareModal({ simulation, @@ -20,9 +20,9 @@ export default function FilmSimulationShareModal({ }) { return ( diff --git a/src/simulation/index.ts b/src/simulation/index.ts index bb571b73..51b63dbd 100644 --- a/src/simulation/index.ts +++ b/src/simulation/index.ts @@ -40,6 +40,11 @@ export const titleForFilmSimulation = ( photoQuantityText(explicitCount ?? photos.length), ].join(' '); +export const shareTextForFilmSimulation = ( + simulation: FilmSimulation, +) => + `Photos shot on Fujifilm ${labelForFilmSimulation(simulation).large}`; + export const descriptionForFilmSimulationPhotos = ( photos: Photo[], dateBased?: boolean, diff --git a/src/site/SiteChecklistClient.tsx b/src/site/SiteChecklistClient.tsx index ebf884a2..5ac61273 100644 --- a/src/site/SiteChecklistClient.tsx +++ b/src/site/SiteChecklistClient.tsx @@ -38,6 +38,7 @@ export default function SiteChecklistClient({ hasTitle, hasDomain, showRepoLink, + showSocial, showFilmSimulations, showExifInfo, isProModeEnabled, @@ -438,6 +439,17 @@ export default function SiteChecklistClient({ Set environment variable to {'"1"'} to hide footer link: {renderEnvVars(['NEXT_PUBLIC_HIDE_REPO_LINK'])} + + Set environment variable to {'"1"'} to hide + {' '} + X button from share modal: + {renderEnvVars(['NEXT_PUBLIC_HIDE_SOCIAL'])} + 0, hasDomain: (process.env.NEXT_PUBLIC_SITE_DOMAIN ?? '').length > 0, showRepoLink: SHOW_REPO_LINK, + showSocial: SHOW_SOCIAL, showFilmSimulations: SHOW_FILM_SIMULATIONS, showExifInfo: SHOW_EXIF_DATA, isProModeEnabled: PRO_MODE_ENABLED, diff --git a/src/site/paths.ts b/src/site/paths.ts index 94a30d3a..c67b0ff3 100644 --- a/src/site/paths.ts +++ b/src/site/paths.ts @@ -6,26 +6,27 @@ import { parameterize } from '@/utility/string'; import { TAG_HIDDEN } from '@/tag'; // Core paths -export const PATH_ROOT = '/'; -export const PATH_GRID = '/grid'; -export const PATH_ADMIN = '/admin'; -export const PATH_API = '/api'; -export const PATH_SIGN_IN = '/sign-in'; -export const PATH_OG = '/og'; +export const PATH_ROOT = '/'; +export const PATH_GRID = '/grid'; +export const PATH_ADMIN = '/admin'; +export const PATH_API = '/api'; +export const PATH_SIGN_IN = '/sign-in'; +export const PATH_OG = '/og'; // Path prefixes -export const PREFIX_PHOTO = '/p'; -export const PREFIX_TAG = '/tag'; -export const PREFIX_CAMERA = '/shot-on'; -export const PREFIX_FILM_SIMULATION = '/film'; -export const PREFIX_FOCAL_LENGTH = '/focal'; +export const PREFIX_PHOTO = '/p'; +export const PREFIX_TAG = '/tag'; +export const PREFIX_CAMERA = '/shot-on'; +export const PREFIX_FILM_SIMULATION = '/film'; +export const PREFIX_FOCAL_LENGTH = '/focal'; // Dynamic paths -const PATH_PHOTO_DYNAMIC = `${PREFIX_PHOTO}/[photoId]`; -const PATH_TAG_DYNAMIC = `${PREFIX_TAG}/[tag]`; -const PATH_CAMERA_DYNAMIC = `${PREFIX_CAMERA}/[make]/[model]`; -const PATH_FILM_SIMULATION_DYNAMIC = `${PREFIX_FILM_SIMULATION}/[simulation]`; -const PATH_FOCAL_LENGTH_DYNAMIC = `${PREFIX_FOCAL_LENGTH}/[focal]`; +const PATH_PHOTO_DYNAMIC = `${PREFIX_PHOTO}/[photoId]`; +const PATH_TAG_DYNAMIC = `${PREFIX_TAG}/[tag]`; +const PATH_CAMERA_DYNAMIC = `${PREFIX_CAMERA}/[make]/[model]`; +// eslint-disable-next-line max-len +const PATH_FILM_SIMULATION_DYNAMIC = `${PREFIX_FILM_SIMULATION}/[simulation]`; +const PATH_FOCAL_LENGTH_DYNAMIC = `${PREFIX_FOCAL_LENGTH}/[focal]`; // Admin paths export const PATH_ADMIN_PHOTOS = `${PATH_ADMIN}/photos`; @@ -34,6 +35,10 @@ export const PATH_ADMIN_TAGS = `${PATH_ADMIN}/tags`; export const PATH_ADMIN_CONFIGURATION = `${PATH_ADMIN}/configuration`; export const PATH_ADMIN_BASELINE = `${PATH_ADMIN}/baseline`; +// Debug paths +export const PATH_OG_ALL = `${PATH_OG}/all`; +export const PATH_OG_SAMPLE = `${PATH_OG}/sample`; + // API paths export const PATH_API_STORAGE = `${PATH_API}/storage`; export const PATH_API_VERCEL_BLOB_UPLOAD = `${PATH_API_STORAGE}/vercel-blob`; @@ -261,7 +266,7 @@ export const isPathAdminConfiguration = (pathname?: string) => export const isPathProtected = (pathname?: string) => checkPathPrefix(pathname, PATH_ADMIN) || checkPathPrefix(pathname, pathForTag(TAG_HIDDEN)) || - pathname === PATH_OG; + checkPathPrefix(pathname, PATH_OG); export const getPathComponents = (pathname = ''): { photoId?: string diff --git a/src/tag/TagShareModal.tsx b/src/tag/TagShareModal.tsx index 5f371090..359ac41e 100644 --- a/src/tag/TagShareModal.tsx +++ b/src/tag/TagShareModal.tsx @@ -2,6 +2,7 @@ import { absolutePathForTag, pathForTag } from '@/site/paths'; import { Photo, PhotoDateRange } from '../photo'; import ShareModal from '@/components/ShareModal'; import TagOGTile from './TagOGTile'; +import { shareTextForTag } from '.'; export default function TagShareModal({ tag, @@ -16,9 +17,9 @@ export default function TagShareModal({ }) { return ( diff --git a/src/tag/index.ts b/src/tag/index.ts index fc85ee32..8498a7e2 100644 --- a/src/tag/index.ts +++ b/src/tag/index.ts @@ -11,9 +11,9 @@ import { } from '@/site/paths'; import { capitalizeWords, convertStringToArray } from '@/utility/string'; -// Reserved/virtual tags -export const TAG_FAVS = 'favs'; // Reserved -export const TAG_HIDDEN = 'hidden'; // Virtual +// Reserved tags +export const TAG_FAVS = 'favs'; +export const TAG_HIDDEN = 'hidden'; export type TagsWithMeta = { tag: string @@ -24,10 +24,7 @@ export const formatTag = (tag?: string) => capitalizeWords(tag?.replaceAll('-', ' ')); export const doesStringContainReservedTags = (tags?: string) => - convertStringToArray(tags)?.some(tag => ( - isTagFavs(tag) || - tag.toLowerCase() === TAG_HIDDEN - )); + convertStringToArray(tags)?.some(tag => isTagFavs(tag) || isTagHidden(tag)); export const titleForTag = ( tag: string, @@ -38,6 +35,9 @@ export const titleForTag = ( photoQuantityText(explicitCount ?? photos.length), ].join(' '); +export const shareTextForTag = (tag: string) => + isTagFavs(tag) ? 'Favorite photos' : `Photos tagged '${tag}'`; + export const sortTags = ( tags: string[], tagToHide?: string, @@ -92,6 +92,8 @@ export const isPhotoFav = ({ tags }: Photo) => tags.some(isTagFavs); export const isPathFavs = (pathname?: string) => getPathComponents(pathname).tag === TAG_FAVS; +export const isTagHidden = (tag: string) => tag.toLowerCase() === TAG_HIDDEN; + export const addHiddenToTags = (tags: TagsWithMeta, hiddenPhotosCount = 0) => { if (hiddenPhotosCount > 0) { return tags diff --git a/src/utility/exif.ts b/src/utility/exif.ts index 60b10288..8760cfe5 100644 --- a/src/utility/exif.ts +++ b/src/utility/exif.ts @@ -1,5 +1,5 @@ import { OrientationTypes, type ExifData } from 'ts-exif-parser'; -import { formatNumberToFraction } from './number'; +import { formatNumberToFraction, roundToString } from './number'; const OFFSET_REGEX = /[+-]\d\d:\d\d/; @@ -32,10 +32,12 @@ export const getAspectRatioFromExif = (data: ExifData): number => { }; export const formatAperture = (aperture?: number) => - aperture ? `ƒ/${aperture}` : undefined; + aperture + ? `ƒ/${roundToString(aperture)}` + : undefined; export const formatIso = (iso?: number) => - iso ? `ISO ${iso}` : undefined; + iso ? `ISO ${iso.toLocaleString()}` : undefined; export const formatExposureTime = (exposureTime = 0) => exposureTime > 0 diff --git a/src/utility/number.ts b/src/utility/number.ts index 2d36bf11..30a3388d 100644 --- a/src/utility/number.ts +++ b/src/utility/number.ts @@ -1,11 +1,18 @@ -export const toFixedNumber = ( +export const roundToString = ( number: number, - digits: number, - base = 10) => { - const pow = Math.pow(base ?? 10, digits); - return Math.round(number * pow) / pow; + place = 1, + includeZero?: boolean, +) => { + const precision = Math.pow(10, place); + const result = Math.round(number * precision) / precision; + return includeZero ? result.toFixed(place) : result.toString(); }; +export const roundToNumber = ( + ...args: Parameters +) => + parseFloat(roundToString(...args)); + const gcd = (a: number, b: number): number => { if (b <= 0.0000001) { return a; diff --git a/src/utility/social.ts b/src/utility/social.ts new file mode 100644 index 00000000..49baefff --- /dev/null +++ b/src/utility/social.ts @@ -0,0 +1,6 @@ +export const generateXPostText = (path: string, text: string) => { + const url = new URL('https://x.com/intent/post'); + url.searchParams.set('text', text); + url.searchParams.set('url', path); + return url.toString(); +};