Skip to content

Commit

Permalink
chore: Misc. client updates.
Browse files Browse the repository at this point in the history
  • Loading branch information
darkobits committed Dec 21, 2021
1 parent 6dbbe53 commit 1c5b400
Show file tree
Hide file tree
Showing 7 changed files with 125 additions and 71 deletions.
23 changes: 4 additions & 19 deletions packages/client/src/components/Greeting.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import { styled } from '@linaria/react';
import { InspiratPhotoResource } from 'inspirat-types';
import ms from 'ms';
import { rgba as polishedRgba } from 'polished';
import * as R from 'ramda';
import React from 'react';

import { useInspirat } from 'hooks/use-inspirat';
import { isPending } from 'hooks/use-storage-item';
import { getPeriodDescriptor } from 'lib/time';
import { compositeTextShadow } from 'lib/typography';
import { rgba } from 'lib/utils';

Expand Down Expand Up @@ -126,24 +123,12 @@ const GreetingBackground = styled.div<GreetingProps>`
* Renders the greeting copy.
*/
const Greeting: React.FunctionComponent = () => {
const { currentPhoto, name } = useInspirat();
const [period, setPeriod] = React.useState(getPeriodDescriptor());
const { currentPhoto, name, period } = useInspirat();

const greeting = name
? `Good ${period}, ${name}.`
: `Good ${period}.`;

/**
* [Effect] Update period every minute.
*/
React.useEffect(() => {
const interval = setInterval(() => {
setPeriod(getPeriodDescriptor());
}, ms('1 minute'));

return () => clearInterval(interval);
}, []);

const greeting = isPending(name)
? `Good ${period}.`
: `Good ${period}, ${name}.`;
const color = rgba(currentPhoto?.palette?.vibrant ?? {r: 0, g: 0, b: 0});

return (
Expand Down
10 changes: 5 additions & 5 deletions packages/client/src/components/Introduction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { Button } from 'react-bootstrap';

import { AnimatedModal } from 'components/AnimatedModal';
import { useInspirat } from 'hooks/use-inspirat';
import { isPending } from 'hooks/use-storage-item';
import { isChromeExtension } from 'lib/utils';


Expand Down Expand Up @@ -34,7 +33,6 @@ export const Introduction: React.FunctionComponent = () => {
if (setHasSeenIntroduction) setHasSeenIntroduction(true);
}, [setHasSeenIntroduction]);


const unsplashLink = React.useMemo(() => (
<a
href="https://unsplash.com/"
Expand All @@ -46,9 +44,11 @@ export const Introduction: React.FunctionComponent = () => {
</a>
), []);


if (!isChromeExtension() || isPending(hasSeenIntroduction)) return null;

if (
!isChromeExtension() ||
hasSeenIntroduction === undefined ||
hasSeenIntroduction
) return null;

return (
<AnimatedModal
Expand Down
4 changes: 3 additions & 1 deletion packages/client/src/components/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ export const Settings: React.FunctionComponent<SettingsProps> = ({ show, onClose
if (onClose) onClose();
}, [setName, onClose, tempName]);

// @ts-expect-error
const version = import.meta.env.GIT_DESC;

return (
<AnimatedModal
Expand All @@ -61,7 +63,7 @@ export const Settings: React.FunctionComponent<SettingsProps> = ({ show, onClose
Inspirat
</h1>
<div className={cx(styles.version, 'text-secondary', 'text-fancy')}>
{import.meta.env.GIT_DESC}
{version}
</div>
</div>
<hr className="bg-secondary mb-4" />
Expand Down
20 changes: 15 additions & 5 deletions packages/client/src/etc/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,31 @@ import type { BackgroundImageOverrides } from 'etc/types';
import type { Color } from 'inspirat-types';


// @ts-expect-error - See: https://github.com/vitejs/vite/issues/6194
const MODE = import.meta.env.MODE;

// @ts-expect-error - See: https://github.com/vitejs/vite/issues/6194
const VITE_TITLE = import.meta.env.VITE_TITLE;

// @ts-expect-error - See: https://github.com/vitejs/vite/issues/6194
const VITE_BUCKET_URL = import.meta.env.VITE_BUCKET_URL;


/**
* Document title to use.
*/
export const TITLE = import.meta.env.MODE === 'production'
export const TITLE = MODE === 'production'
? process.env.TITLE
: import.meta.env.VITE_TITLE;
: VITE_TITLE;
if (!TITLE) throw new Error('TITLE is not set.');


/**
* AWS S3 Bucket URL.
*/
export const BUCKET_URL = import.meta.env.MODE === 'production'
export const BUCKET_URL = MODE === 'production'
? process.env.BUCKET_URL
: import.meta.env.VITE_BUCKET_URL;
: VITE_BUCKET_URL;
if (!BUCKET_URL) throw new Error('BUCKET_URL is not set.');


Expand Down Expand Up @@ -56,7 +66,7 @@ export const CACHE_TTL = ms('1 day');
/**
* Storage key used to cache the photo collection.
*/
export const COLLECTION_CACHE_KEY = 'photoCollection';
export const COLLECTION_CACHE_KEY = 'photoCollections';


/**
Expand Down
36 changes: 31 additions & 5 deletions packages/client/src/hooks/use-inspirat.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { InspiratPhotoResource } from 'inspirat-types';
import ms from 'ms';
import prettyMs from 'pretty-ms';
import * as R from 'ramda';
import React from 'react';
Expand All @@ -7,13 +8,14 @@ import useAsyncEffect from 'use-async-effect';

import { PhotoUrls } from 'etc/types';
import useQuery from 'hooks/use-query';
import useStorageItem from 'hooks/use-storage-item';
import withNamespace from 'hooks/use-storage-item';
import {
getPhotoCollections,
getCurrentPhotoFromCollection,
getCurrentPhotoFromCache
} from 'lib/photos';
import {
getPeriodDescriptor,
midnight,
now
} from 'lib/time';
Expand Down Expand Up @@ -78,6 +80,11 @@ export interface InspiratHook {
*/
setName: (value: string) => void;

/**
* Current period of the day ('morning', 'afternoon', 'evening').
*/
period: string;

/**
* Allows other components to set the day offset to a value by using the
* 'increment' or 'decrement' actions.
Expand Down Expand Up @@ -106,16 +113,22 @@ export interface InspiratHook {
const preloadedPhotos = new Set<string>();


// @ts-expect-error
export const useInspirat = singletonHook({} as InspiratHook, () => {
const [currentPhotoUrls, setCurrentPhotoUrls] = React.useState<PhotoUrls>();
const useStorageItem = withNamespace('inspirat');


const initialValue = {} as InspiratHook;


export const useInspirat = singletonHook(initialValue, () => {
const [hasSeenIntroduction, setHasSeenIntroduction] = useStorageItem<boolean | undefined>('hasSeenIntroduction', false);
const [currentPhotoFromState, setCurrentPhoto] = React.useState<InspiratPhotoResource>();
const [currentPhotoUrls, setCurrentPhotoUrls] = React.useState<PhotoUrls>();
const [shouldResetPhoto, resetPhoto] = React.useState(0);
const [numPhotos, setNumPhotos] = React.useState(0);
const [showDevTools, setShowDevTools] = React.useState(false);
const [isLoadingPhotos, setIsLoadingPhotos] = React.useState(false);
const [name, setName] = useStorageItem<string>('name');
const [hasSeenIntroduction, setHasSeenIntroduction] = useStorageItem<boolean>('hasSeenIntroduction', false);
const [period, setPeriod] = React.useState(getPeriodDescriptor());
const query = useQuery();


Expand Down Expand Up @@ -312,6 +325,18 @@ export const useInspirat = singletonHook({} as InspiratHook, () => {
}, []);


/**
* [Effect] Update period.
*/
React.useEffect(() => {
const interval = setInterval(() => {
setPeriod(getPeriodDescriptor());
}, ms('30 seconds'));

return () => clearInterval(interval);
}, []);


// ----- Hook API ------------------------------------------------------------

return {
Expand All @@ -326,6 +351,7 @@ export const useInspirat = singletonHook({} as InspiratHook, () => {
isLoadingPhotos,
name,
setName,
period,
currentPhoto: currentPhotoFromState,
currentPhotoUrls,
setCurrentPhoto,
Expand Down
101 changes: 66 additions & 35 deletions packages/client/src/hooks/use-storage-item.ts
Original file line number Diff line number Diff line change
@@ -1,58 +1,89 @@
import localforage from 'localforage';
import React from 'react';
import useAsyncEffect from 'use-async-effect';

import storage from 'lib/storage';

/**
* @private
*
* Value we compare to to determine if a storage value is pending sync.
*/
const PENDING = Symbol('PENDING');
type LocalForage = ReturnType<typeof localforage['createInstance']>;
type UseStorageItem<T = any> = [T, React.Dispatch<React.SetStateAction<T>>];


/**
* Returns true if the provided value is pending.
*/
export function isPending(value: any) {
return value === PENDING;
}

const instances = new Map<string, LocalForage>();

type HookReturnValue<T = any> = [
T,
React.Dispatch<React.SetStateAction<T>>
];

/**
* Provided a key, returns a tuple value and setter function that will sync the
* provided value to Local Storage.
*
* TODO: Make own package.
*/
function useStorageItem<T = any>(key: string): HookReturnValue<T | typeof PENDING | undefined>;
function useStorageItem<T = any>(key: string, initialValue: T): HookReturnValue<T | typeof PENDING>;
function useStorageItem<T = any>(key: string, initialValue?: T) {
const [localValue, setLocalValue] = React.useState<T | typeof PENDING | undefined>(PENDING);
function useStorageItem<T = any>(namespace: string, key: string): UseStorageItem<T | undefined>;
function useStorageItem<T = any>(namespace: string, key: string, initialValue: T): UseStorageItem<T>;
function useStorageItem<T = any>(namespace: string, key: string, initialValue?: T) {
const [localValue, setLocalValue] = React.useState<T | undefined>();
const [storage, setStorage] = React.useState<LocalForage>();

/**
* Updates the tracked value locally and in storage.
*/
const setValue: React.Dispatch<React.SetStateAction<T>> = React.useCallback(value => {
if (!storage) return;

const setValue: React.Dispatch<React.SetStateAction<T>> = value => {
void storage.setItem(key, value);
setLocalValue(value as T);
};
}, [storage]);

useAsyncEffect(async () => {
const valueFromStorage = await storage.getItem<T>(key);

if (valueFromStorage !== null) {
setLocalValue(valueFromStorage);
} else if (initialValue !== undefined) {
setValue(initialValue);
/**
* Initialize storage instance.
*/
React.useEffect(() => {
if (!instances.has(namespace)) {
instances.set(namespace, localforage.createInstance({
driver: localforage.LOCALSTORAGE,
name: namespace,
version: 1
}));
}

}, [setLocalValue]);
setStorage(instances.get(namespace));
}, [setStorage]);


/**
* Sync value from storage to local state.
*/
React.useEffect(() => {
if (!storage) return;

void storage?.getItem<T>(key).then(valueFromStorage => {
// If we got a value back from storage, set the local value accordingly.
if (valueFromStorage !== null) {
setLocalValue(valueFromStorage);
return;
}

// If storage was empty and an initial value was provided, set it locally
// and in storage.
if (initialValue !== undefined) {
setValue(initialValue);
return;
}
});
}, [storage, setValue]);


return [localValue, setValue];
}


export default useStorageItem;
export function withNamespace(namespace: string) {
function boundUseStorageItem<T = any>(key: string): UseStorageItem<T | undefined>;
function boundUseStorageItem<T = any>(key: string, initialValue: T): UseStorageItem<T>;
// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
function boundUseStorageItem<T = any>(key: string, initialValue?: T) {
// eslint-disable-next-line react-hooks/rules-of-hooks
return useStorageItem(namespace, key, initialValue);
}

return boundUseStorageItem;
}


export default withNamespace;
2 changes: 1 addition & 1 deletion packages/client/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Color, InspiratPhotoResource } from 'inspirat-types';
import { rgba as polishedRgba, parseToRgb } from 'polished';
import queryString from 'query-string';
import * as R from 'ramda';
// @ts-expect-error
// @ts-expect-error - No declarations for this package.
import urlParseLax from 'url-parse-lax';

import { QUALITY_LQIP, QUALITY_FULL } from 'etc/constants';
Expand Down

0 comments on commit 1c5b400

Please sign in to comment.