Skip to content

Commit

Permalink
feat: add posthog (closes #220) (#221)
Browse files Browse the repository at this point in the history
gempain authored Apr 2, 2021

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent b14f1ab commit 5995fbb
Showing 34 changed files with 362 additions and 57 deletions.
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -23,6 +23,7 @@ WORKDIR /app/server

ENV MELI_URL_INTERNAL=http://localhost:3001
ENV MELI_UI_DIR=/app/ui
ENV MELI_POSTHOG_ENABLED="true"

# Caddy defaults, copied from official Dockerfile
# https://github.com/caddyserver/caddy-docker/blob/2093c4a571bfe356447008d229195eb7063232b2/2.3/alpine/Dockerfile
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -16,7 +16,8 @@
"build:ui": "npm run build --prefix ui",
"test": "npm run test:ui",
"test:ui": "npm run test --prefix server",
"lint": "npm run lint --prefix server"
"lint": "npm run lint --prefix server",
"lint:fix": "npm run lint:fix --prefix server"
},
"keywords": [
"meli",
73 changes: 71 additions & 2 deletions server/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions server/package.json
Original file line number Diff line number Diff line change
@@ -96,6 +96,7 @@
"passport-custom": "^1.1.1",
"passport-google-oauth20": "^2.0.0",
"passport-oauth2": "^1.5.0",
"posthog-node": "^1.0.7",
"prom-client": "^12.0.0",
"qs": "^6.9.4",
"queue": "^6.0.1",
2 changes: 1 addition & 1 deletion server/src/auth/handlers/get-auth-methods.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import request from 'supertest';
import { MeliServer } from '../../server';
import { MeliServer } from '../../createServer';
import { spyOnVerifyToken } from '../../../tests/utils/spyon-verifytoken';
import { testServer } from '../../../tests/test-server';
import { authMethods } from '../passport/auth-methods';
2 changes: 1 addition & 1 deletion server/src/auth/handlers/sign-out.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import request from 'supertest';
import { MeliServer } from '../../server';
import { MeliServer } from '../../createServer';
import { spyOnVerifyToken } from '../../../tests/utils/spyon-verifytoken';
import { testServer } from '../../../tests/test-server';

8 changes: 5 additions & 3 deletions server/src/server.ts → server/src/createServer.ts
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ import cors from 'cors';
import express, { Express } from 'express';
import rateLimit from 'express-rate-limit';
import helmet from 'helmet';
import { createServer, Server } from 'http';
import { createServer as createHttpServer, Server } from 'http';
import morgan from 'morgan';
import passport from 'passport';
import { Logger } from './commons/logger/logger';
@@ -24,6 +24,7 @@ import { authorizeApiReq } from './auth/handlers/authorize-api-req';
import './auth/passport';
import { createIoServer } from './socket/create-io-server';
import { initSocketRooms } from './socket/socket-rooms';
import { initPosthog } from './posthog/init-posthog';

const logger = new Logger('meli.api:server');

@@ -50,8 +51,9 @@ export interface MeliServer {
stop: () => void;
}

export async function server(): Promise<MeliServer> {
export async function createServer(): Promise<MeliServer> {
await AppDb.init();
await initPosthog();
await migrate(AppDb.client, AppDb.db);
setupDbIndexes().catch(err => logger.error('Could not setup indexes indexes', err));
await configureCaddy();
@@ -89,7 +91,7 @@ export async function server(): Promise<MeliServer> {
app.use(Sentry.Handlers.errorHandler());
app.use(handleError);

const httpServer = createServer(app);
const httpServer = createHttpServer(app);
createIoServer(httpServer);

initSocketRooms();
8 changes: 4 additions & 4 deletions server/src/entities/orgs/handlers/create-org.spec.ts
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ import { testServer } from '../../../../tests/test-server';
import { spyOnCollection } from '../../../../tests/utils/spyon-collection';
import { spyOnVerifyToken } from '../../../../tests/utils/spyon-verifytoken';
import * as _emitEvent from '../../../events/emit-event';
import { MeliServer } from '../../../server';
import { MeliServer } from '../../../createServer';
import { User } from '../../users/user';

import request from 'supertest';
@@ -50,7 +50,7 @@ describe('createOrg', () => {
.post('/api/v1/orgs')
.set('Cookie', ['auth=testToken'])
.send({
name: 'Test Organization'
name: 'Test Organization',
});


@@ -75,7 +75,7 @@ describe('createOrg', () => {
userId: 'authenticatedUserId',
admin: true,
name: 'Authenticated User',
email: 'authenticated@test.tst'
email: 'authenticated@test.tst',
}));

expect(teams.insertOne).toHaveBeenCalled();
@@ -93,7 +93,7 @@ describe('createOrg', () => {
.post('/api/v1/orgs')
.set('Cookie', ['auth=testToken'])
.send({
name: 'Test Organization'
name: 'Test Organization',
});


Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@ import { spyOnCollection } from '../../../../../tests/utils/spyon-collection';
import { spyOnIsAdminOrOwner } from '../../../../../tests/utils/spyon-isadminorowner';
import { AUTHENTICATED_USER_ID, spyOnVerifyToken } from '../../../../../tests/utils/spyon-verifytoken';
import * as _emitEvent from '../../../../events/emit-event';
import { MeliServer } from '../../../../server';
import { MeliServer } from '../../../../createServer';

// jest.mock('../../../../env/env', () => ({ env: testEnv }));

Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ import { spyOnCollection } from '../../../../../tests/utils/spyon-collection';
import { spyOnVerifyToken } from '../../../../../tests/utils/spyon-verifytoken';
import * as _configureSiteBranchInCaddy from '../../../../caddy/configuration';
import * as _emitEvent from '../../../../events/emit-event';
import { MeliServer } from '../../../../server';
import { MeliServer } from '../../../../createServer';
import * as _linkBranchToRelease from '../../link-branch-to-release';
import { linkBranchToRelease } from '../../link-branch-to-release';
import { EventType } from '../../../../events/event-type';
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@ import { spyOnVerifyToken } from '../../../../../tests/utils/spyon-verifytoken';
import * as _removeSiteBranchFromCaddy from '../../../../caddy/configuration';
import { removeSiteBranchFromCaddy } from '../../../../caddy/configuration';
import * as _emitEvent from '../../../../events/emit-event';
import { MeliServer } from '../../../../server';
import { MeliServer } from '../../../../createServer';
import { EventType } from '../../../../events/event-type';
import { canAdminSiteGuard } from '../../guards/can-admin-site-guard';
import { branchExistsGuard } from '../../guards/branch-exists-guard';
18 changes: 9 additions & 9 deletions server/src/entities/teams/handlers/sites/create-site.spec.ts
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@ import { spyOnIsAdminOrOwner } from '../../../../../tests/utils/spyon-isadminoro
import { AUTHENTICATED_USER_ID, spyOnVerifyToken } from '../../../../../tests/utils/spyon-verifytoken';
import * as _configureSiteInCaddy from '../../../../caddy/configuration';
import * as _emitEvent from '../../../../events/emit-event';
import { MeliServer } from '../../../../server';
import { MeliServer } from '../../../../createServer';

// jest.mock('../../../../env/env', () => ({ env: testEnv }));

@@ -27,7 +27,7 @@ describe('createSite', () => {
it('should create a site', async () => {
const teams = spyOnCollection('Teams', {
countDocuments: jest.fn().mockReturnValue(Promise.resolve(1)),
findOne: jest.fn().mockReturnValue(Promise.resolve({orgId: 'organization-id'})),
findOne: jest.fn().mockReturnValue(Promise.resolve({ orgId: 'organization-id' })),
});
const members = spyOnCollection('Members', {
countDocuments: jest.fn().mockReturnValue(Promise.resolve(1)),
@@ -43,7 +43,7 @@ describe('createSite', () => {
.post('/api/v1/teams/team-id/sites')
.set('Cookie', ['auth=testToken'])
.send({
name: 'test-site'
name: 'test-site',
});


@@ -79,19 +79,19 @@ describe('createSite', () => {
.post('/api/v1/teams/team-id/sites')
.set('Cookie', ['auth=testToken'])
.send({
name: 'test-site'
name: 'test-site',
});


expect(response.status).toEqual(404);
expect(teams.countDocuments).toHaveBeenCalledWith({_id: 'team-id'}, expect.anything());
expect(teams.countDocuments).toHaveBeenCalledWith({ _id: 'team-id' }, expect.anything());
});


it('should check if the user can administrate the team', async () => {
const teams = spyOnCollection('Teams', {
countDocuments: jest.fn().mockReturnValue(Promise.resolve(1)),
findOne: jest.fn().mockReturnValue(Promise.resolve({orgId: 'organization-id'})),
findOne: jest.fn().mockReturnValue(Promise.resolve({ orgId: 'organization-id' })),
});
const isAdminOrOwner = spyOnIsAdminOrOwner(false);

@@ -100,7 +100,7 @@ describe('createSite', () => {
.post('/api/v1/teams/team-id/sites')
.set('Cookie', ['auth=testToken'])
.send({
name: 'test-site'
name: 'test-site',
});


@@ -112,7 +112,7 @@ describe('createSite', () => {
it('should validate the site', async () => {
const teams = spyOnCollection('Teams', {
countDocuments: jest.fn().mockReturnValue(Promise.resolve(1)),
findOne: jest.fn().mockReturnValue(Promise.resolve({orgId: 'organization-id'})),
findOne: jest.fn().mockReturnValue(Promise.resolve({ orgId: 'organization-id' })),
});
const isAdminOrOwner = spyOnIsAdminOrOwner(true);

@@ -121,7 +121,7 @@ describe('createSite', () => {
.post('/api/v1/teams/team-id/sites')
.set('Cookie', ['auth=testToken'])
.send({
name: 'invalid site name'
name: 'invalid site name',
});

expect(response.status).toEqual(400);
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ import request from 'supertest';
import { testServer } from '../../../../../tests/test-server';
import { spyOnCollection } from '../../../../../tests/utils/spyon-collection';
import { spyOnVerifyToken } from '../../../../../tests/utils/spyon-verifytoken';
import { MeliServer } from '../../../../server';
import { MeliServer } from '../../../../createServer';
import * as _teamExistsGuard from '../../guards/team-exists-guard';
import * as _canReadTeamGuard from '../../guards/can-read-team-guard';
import * as _serializeSite from '../../../sites/serialize-site';
3 changes: 3 additions & 0 deletions server/src/env/env-spec.ts
Original file line number Diff line number Diff line change
@@ -272,4 +272,7 @@ export const envSpec: EnvSpec<Env> = {
MELI_GOOGLE_RECAPTCHA_SECRET_KEY: {
schema: string().optional(),
},
MELI_POSTHOG_ENABLED: {
schema: boolean().optional().default(false),
},
};
1 change: 1 addition & 0 deletions server/src/env/env.ts
Original file line number Diff line number Diff line change
@@ -73,6 +73,7 @@ export interface Env {
MELI_MULTER_FORM_LIMITS: MulterLimitOptions;
MELI_GOOGLE_RECAPTCHA_SITE_KEY: string;
MELI_GOOGLE_RECAPTCHA_SECRET_KEY: string;
MELI_POSTHOG_ENABLED: boolean;
}

export const env: Env = parseEnv(envSpec);
4 changes: 2 additions & 2 deletions server/src/index.ts
Original file line number Diff line number Diff line change
@@ -11,7 +11,7 @@ require('source-map-support/register');
require('dotenv/config');
require('./commons/force-chalk-colors');

const { server } = require('./server');
const { createServer } = require('./createServer');

// eslint-disable-next-line no-console
server().catch(console.error);
createServer().catch(console.error);
31 changes: 31 additions & 0 deletions server/src/posthog/init-posthog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { v4 as uuid } from 'uuid';
import { postHogId } from './posthog';
import { sendHeartbeat } from './send-heartbeat';
import { AppDb } from '../db/db';

export interface AppInfo {
_id: string;
value: string;
}

export const AppInfos = () => AppDb.db.collection<AppInfo>('app-info');

const appInfoKey = 'install_id';

export async function initPosthog() {
// id
const appInfo = await AppInfos().findOne({ _id: appInfoKey });
if (appInfo) {
postHogId.id = appInfo.value;
} else {
postHogId.id = uuid();
await AppInfos().insertOne({
_id: appInfoKey,
value: postHogId.id,
});
}

// heartbeat
sendHeartbeat();
setInterval(sendHeartbeat, 86400000); // every day
}
14 changes: 14 additions & 0 deletions server/src/posthog/posthog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import PostHog from 'posthog-node';
import { env } from '../env/env';

export const postHog = new PostHog(
'-BcCDFlG6nIchkTWROH5C3iplPWRjdEwrb6wpQKKwDg',
{
host: 'https://posthog.meli.sh',
enable: env.MELI_POSTHOG_ENABLED,
},
);

export const postHogId = {
id: undefined,
};
15 changes: 15 additions & 0 deletions server/src/posthog/send-heartbeat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { postHog, postHogId } from './posthog';
import { Logger } from '../commons/logger/logger';

const logger = new Logger('app.posthog:sendHeartbeat');

export function sendHeartbeat() {
logger.debug('sending heartbeat');
postHog.capture({
event: 'heartbeat',
distinctId: postHogId.id,
properties: {
version: BUILD_INFO.version,
},
});
}
2 changes: 1 addition & 1 deletion server/tests/test-server.ts
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@ import { Logger } from '../src/commons/logger/logger';
import { handleError } from '../src/commons/utils/handle-error';
import { env } from '../src/env/env';
import routes from '../src/routes';
import { MeliServer } from '../src/server';
import { MeliServer } from '../src/createServer';
import { authorizeReq } from '../src/auth/handlers/authorize-req';
import { authorizeApiReq } from '../src/auth/handlers/authorize-api-req';
import '../src/auth/passport';
7 changes: 7 additions & 0 deletions ui/src/App.module.scss
Original file line number Diff line number Diff line change
@@ -30,3 +30,10 @@ $header-height: 70px;
display: flex;
flex-grow: 1;
}

.banner {
position: absolute;
top: 0;
left: 0;
z-index: 10;
}
20 changes: 11 additions & 9 deletions ui/src/App.tsx
Original file line number Diff line number Diff line change
@@ -26,6 +26,7 @@ import { UserView } from './components/user/UserView';
import { PrivateRoute } from './commons/components/PrivateRoute';
import { FullPageCentered } from './commons/components/FullPageCentered';
import { Loader } from './commons/components/Loader';
import { PosthogWarning } from './posthog/PosthogWarning';

function Header() {
const { user } = useAuth();
@@ -37,15 +38,15 @@ function Header() {
<div className="col d-flex align-items-center justify-content-between">
{user && (
<>
<UserInfo />
<UserInfo/>
{currentOrg && (
<div className="d-flex align-items-center">
<AddTeam>
<ButtonIcon>
<FontAwesomeIcon icon={faPlus} />
<FontAwesomeIcon icon={faPlus}/>
</ButtonIcon>
</AddTeam>
<Search />
<Search/>
</div>
)}
</>
@@ -65,14 +66,15 @@ export function App() {
<FullPageCentered>
<p>
Initializing
<Loader className="ml-2" />
<Loader className="ml-2"/>
</p>
</FullPageCentered>
) : (
<div className={styles.app} id="app">
<Header />
<PosthogWarning/>
<Header/>
{currentOrg && (
<SideBar className={styles.sidebar} />
<SideBar className={styles.sidebar}/>
)}
<main className={styles.main}>
<Switch>
@@ -142,12 +144,12 @@ export function App() {
redirectTo="/orgs"
/>

<Route path="/legal" component={Legals} />
<Route path="/legal" component={Legals}/>

<Route component={NotFound} />
<Route component={NotFound}/>
</Switch>
</main>
<Footer />
<Footer/>
</div>
);
}
2 changes: 1 addition & 1 deletion ui/src/commons/components/modals/AppModal.module.scss
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@
bottom: 0;
left: 0;
right: 0;
background: transparentize($_primary, 0.8);
background: transparentize($_primary, 0.7);
}

.container {
23 changes: 9 additions & 14 deletions ui/src/commons/components/modals/AppModal.tsx
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@ import React, { useEffect } from 'react';
import Modal from 'react-modal';
import classNames from 'classnames';
import styles from './AppModal.module.scss';
import { useBlur } from '../../../providers/BlurProvider';

export type AppModalProps = {
isOpen: boolean;
@@ -13,16 +14,6 @@ export type AppModalProps = {
[key: string]: any;
};

const blurElId = 'blur-overlay';

function blurBackground() {
document.getElementById(blurElId).setAttribute('data-blur', 'true');
}

function unblurBackground() {
document.getElementById(blurElId).removeAttribute('data-blur');
}

export function AppModal({
title,
children,
@@ -32,20 +23,24 @@ export function AppModal({
footer,
...otherProps
}: AppModalProps) {
const { blur, unblur } = useBlur();

const close = () => {
unblurBackground();
if (closeModal) {
closeModal();
unblur();
}
};

// TODO for some strange reason, isOpen goes to false and unblur is called
// this is why modal blur is broken
useEffect(() => {
if (isOpen) {
blurBackground();
blur();
} else {
unblurBackground();
unblur();
}
}, [isOpen]);
}, [isOpen, blur, unblur]);

return (
<Modal
6 changes: 4 additions & 2 deletions ui/src/commons/components/modals/CardModal.tsx
Original file line number Diff line number Diff line change
@@ -11,6 +11,7 @@ export type AppModalProps = {
footer?: any;
closeModal?: (...args: string[]) => void;
className?: string;
cardClassName?: string;
[key: string]: any;
};

@@ -20,6 +21,7 @@ export function CardModal({
isOpen,
closeModal,
className,
cardClassName,
footer,
...otherProps
}: AppModalProps) {
@@ -30,10 +32,10 @@ export function CardModal({
className={className}
{...otherProps}
>
<div className={classNames('card', styles.card)}>
<div className={classNames('card', styles.card, cardClassName)}>
<div className="card-header d-flex align-items-center justify-content-between">
<strong className="mr-5">{title}</strong>
<CloseModal onClick={closeModal} />
<CloseModal onClick={closeModal}/>
</div>

<div className={`card-body ${styles.content}`}>{children}</div>
5 changes: 3 additions & 2 deletions ui/src/index.tsx
Original file line number Diff line number Diff line change
@@ -13,6 +13,7 @@ import { routerHistory } from './providers/history';
import { SocketProvider } from './websockets/SocketProvider';
import { isSentryEnabled, SENTRY_CONFIGURED, SentryProvider } from './commons/sentry/SentryProvider';
import { OrgProvider } from './providers/OrgProvider';
import { BlurProvider } from './providers/BlurProvider';

if (SENTRY_CONFIGURED) {
if (isSentryEnabled()) {
@@ -73,15 +74,15 @@ const app = (
ReactDOM.render(
// <React.StrictMode>
<>
<div id="blur-overlay">
<BlurProvider>
{SENTRY_CONFIGURED ? (
<SentryProvider>
{app}
</SentryProvider>
) : (
app
)}
</div>
</BlurProvider>
<Toasts/>
</>,
// </React.StrictMode>,
30 changes: 30 additions & 0 deletions ui/src/posthog/PosthogWarning.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
@import "src/styles/variables";

.container {
padding: 3rem;
background: linear-gradient(180deg, #200c39, #220f3f);
color: $light;
}

.logos {
display: flex;
align-items: center;
justify-content: center;
}

.message {
margin-top: 3rem;
}

.posthogLogo {
height: 35px;
}

.plus {
margin: 0 2rem;
font-size: 2rem;
}

.meliLogo {
height: 35px;
}
37 changes: 37 additions & 0 deletions ui/src/posthog/PosthogWarning.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from "react";
import { ExternalLink } from '../commons/components/ExternalLink';
import { useLocalStorage } from '../utils/use-local-storage';
import styles from './PosthogWarning.module.scss';
import classNames from 'classnames';
import { AppModal } from '../commons/components/modals/AppModal';
import postHogLogo from './posthog.svg';
import meliLogo from './meli.svg';
import { faPlus } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";

export function PosthogWarning({ className }: {
className?: string;
}) {
const [show, setShow] = useLocalStorage('posthog.warning.show', true);
return (
<AppModal isOpen={show}>
<div className={classNames(styles.container, className)}>
<div className={styles.logos}>
<img src={postHogLogo} alt="posthog" className={styles.posthogLogo}/>
<FontAwesomeIcon icon={faPlus} className={styles.plus}/>
<img src={meliLogo} alt="meli" className={styles.meliLogo}/>
</div>
<div className="mt-4 text-center">
We've added <ExternalLink href="https://github.com/PostHog/posthog">PostHog</ExternalLink> to Meli.
It helps us know which versions are being used in production and how many active installations are being deployed across the world.
You may opt-out of this feature. Please review <ExternalLink href="https://github.com/getmeli/meli/issues/220">this thread</ExternalLink> for more info.
</div>
<div className="mt-4 text-center">
<button type="button" className="btn btn-success" onClick={() => setShow(false)}>
I've read the thread
</button>
</div>
</div>
</AppModal>
)
}
5 changes: 5 additions & 0 deletions ui/src/posthog/meli.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions ui/src/posthog/posthog.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions ui/src/providers/BlurProvider.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.blur {
filter: blur(8px);
}
32 changes: 32 additions & 0 deletions ui/src/providers/BlurProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React, { createContext, useContext, useState } from "react";
import styles from './BlurProvider.module.scss';
import classNames from 'classnames';

interface Context {
isBlurred: boolean;
blur: () => void;
unblur: () => void;
}

const context = createContext(undefined);
export const useBlur = () => useContext<Context>(context);

export function BlurProvider({ children, ...props }) {
const [enabled, setEnabled] = useState(false);
return (
<context.Provider
value={{
isBlurred: enabled,
blur: () => setEnabled(true),
unblur: () => setEnabled(false),
}}
{...props}
>
<div className={classNames({
[styles.blur]: enabled,
})}>
{children}
</div>
</context.Provider>
)
}
2 changes: 1 addition & 1 deletion ui/src/styles/variables.scss
Original file line number Diff line number Diff line change
@@ -111,7 +111,7 @@ $input-height: 50px;
$input-focus-border-color: $primary;
$input-focus-box-shadow: none;

$btn-border-radius: $border-radius;
$btn-border-radius: 0;
$btn-font-weight: bold;
$btn-line-height: 100%;
$btn-border-width: 2px;
43 changes: 43 additions & 0 deletions ui/src/utils/use-local-storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { useState } from 'react';

function readFromStorage(key: string): any {
try {
const item = localStorage.getItem(key);
return JSON.parse(item);
} catch (e) {
console.log(`Could not read ${key} from local storage`, e);
return undefined;
}
}

function writeToStorage(key: string, value: any): void {
try {
const val = JSON.stringify(value);
localStorage.setItem(key, val);
} catch (e) {
console.log(`Could not write ${key} to local storage`, value, e);
}
}

function storageHasKey(key: string): boolean {
return !!localStorage.getItem(key);
}

function init(key: string, value: any): any {
if (storageHasKey(key)) {
return readFromStorage(key);
}
writeToStorage(key, value);
return value;
}

export function useLocalStorage<T>(key: string, initialValue?: T): [T, (value: T) => void] {
const [value, setValue] = useState<T>(init(key, initialValue));

const setValueProxy = (newValue: T) => {
setValue(newValue);
writeToStorage(key, newValue);
};

return [value, setValueProxy];
}

0 comments on commit 5995fbb

Please sign in to comment.