Skip to content

Commit

Permalink
Move use-notification to monorepo (#120)
Browse files Browse the repository at this point in the history
  • Loading branch information
aaronccasanova committed Aug 4, 2023
1 parent f62df20 commit cde7145
Show file tree
Hide file tree
Showing 11 changed files with 447 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/quiet-mugs-jog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'use-notification': patch
---

Moved `use-notification` to monorepo
11 changes: 11 additions & 0 deletions hooks/use-notification/.babelrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"presets": [
[
"@aacc/babel-preset",
{
"typescript": true,
"react": true
}
]
]
}
12 changes: 12 additions & 0 deletions hooks/use-notification/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* @type {import('eslint').Linter.Config}
*/
module.exports = {
root: true,
extends: ['@aacc/eslint-config/typescript'],
parserOptions: {
tsconfigRootDir: __dirname,
project: 'tsconfig.eslint.json',
},
ignorePatterns: ['node_modules', 'dist'],
}
83 changes: 83 additions & 0 deletions hooks/use-notification/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# use-notification

React hook wrapping the Web Notification API.

## Usage

```bash
npm install use-notification
```

```tsx
import { useNotification } from 'use-notification'
```

## useNotification ReturnType

```tsx
interface UseNotificationReturnType {
/**
* Represents the permission status for displaying web notifications on
* the current origin.
*
* @example 'default' | 'denied' | 'granted'
*/
permission: NotificationPermission | null
/**
* Any error thrown while initializing the Notification constructor.
*/
error: Error | null
/**
* Requests permission to display web notifications.
*/
requestPermission: () => Promise<void>
/**
* Triggers a web notification.
*
* Note: The function signature matches the web Notification constructor one
* to one. See the MDN documentation for more information:
* https://developer.mozilla.org/en-US/docs/Web/API/Notification
*/
notify: (
title: Notification['title'],
options?: NotificationOptions,
) => Notification | null
}
```

## Examples

Note: The API is intentionally minimal and unopinionated to ensure the hook is
flexible enough to handle multiple use cases. The following examples demonstrate
some common configurations.

### Example: Simple Notification

[Demo](https://1pb5m.csb.app/)
[Code Sandbox](https://codesandbox.io/s/simple-notification-1pb5m)

```tsx
import { useNotification } from 'use-notification'

function App() {
const { permission, error, requestPermission, notify } = useNotification()

if (error) {
return (
<div>
Notification Error:
<pre>{error.message}</pre>
</div>
)
}

return (
<div>
{permission === 'default' && (
<button onClick={requestPermission}>Enable Notifications</button>
)}
<button onClick={() => notify('Hi')}>Notify</button>
</div>
)
}
```
63 changes: 63 additions & 0 deletions hooks/use-notification/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
{
"name": "use-notification",
"version": "0.2.2",
"description": "",
"author": "Aaron Casanova <aaronccasanova@gmail.com>",
"license": "MIT",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.mjs",
"types": "dist/types/index.d.ts",
"exports": {
".": {
"types": "./dist/types/index.d.ts",
"import": "./dist/esm/index.mjs",
"require": "./dist/cjs/index.js"
}
},
"scripts": {
"dev": "npm-run-all --parallel 'build:* -- --watch'",
"build": "npm-run-all --parallel build:*",
"build:js": "rollup -c",
"build:types": "tsc --emitDeclarationOnly",
"type-check": "tsc --noEmit",
"type-check:watch": "npm run type-check -- --watch",
"lint": "TIMING=1 eslint . --ext .js,.ts --cache",
"prepublishOnly": "npm run build"
},
"files": [
"dist"
],
"peerDependencies": {
"react": "^17.0.0"
},
"dependencies": {},
"devDependencies": {
"@aacc/babel-preset": "*",
"@aacc/browserslist-config": "*",
"@aacc/eslint-config": "*",
"@aacc/tsconfigs": "*",
"@rollup/plugin-babel": "^5.3.1",
"@rollup/plugin-commonjs": "^21.1.0",
"@rollup/plugin-node-resolve": "^13.2.1",
"@types/react": "^17.0.0",
"react": "^17.0.0",
"rollup": "^2.70.2",
"typescript": "^4.7.3"
},
"browserslist": [
"extends @aacc/browserslist-config"
],
"publishConfig": {
"access": "public",
"@aacc:registry": "https://registry.npmjs.org"
},
"repository": {
"type": "git",
"url": "git+https://github.com/aaronccasanova/aacc.git",
"directory": "hooks/use-notification"
},
"bugs": {
"url": "https://github.com/aaronccasanova/aacc/issues"
},
"homepage": "https://github.com/aaronccasanova/aacc/blob/main/hooks/use-notification/README.md"
}
47 changes: 47 additions & 0 deletions hooks/use-notification/rollup.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import path from 'path'

import nodeResolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import babel from '@rollup/plugin-babel'

import pkg from './package.json'

const extensions = ['.js', '.jsx', '.ts', '.tsx']

/**
* @type {import('rollup').RollupOptions}
*/
export default {
input: 'src/index.tsx',
output: [
{
format: /** @type {const} */ ('cjs'),
entryFileNames: '[name][assetExtname].js',
dir: path.dirname(pkg.main),
preserveModules: true,
},
{
format: /** @type {const} */ ('es'),
entryFileNames: '[name][assetExtname].mjs',
dir: path.dirname(pkg.module),
preserveModules: true,
},
],
plugins: [
// Allows node_modules resolution
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
nodeResolve({ extensions }),
// Allow bundling cjs modules. Rollup doesn't understand cjs
commonjs(),
// Compile TypeScript/JavaScript files
babel({
extensions,
babelHelpers: 'bundled',
include: ['src/**/*'],
}),
],
external: [
...Object.keys(pkg.dependencies ?? {}),
...Object.keys(pkg.peerDependencies ?? {}),
],
}
143 changes: 143 additions & 0 deletions hooks/use-notification/src/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/**
* Notification API Docs:
* https://developer.mozilla.org/en-US/docs/Web/API/Notification
*/
import React from 'react'

import { isSupported, useIsomorphicLayoutEffect } from './utils'

interface NotificationState {
permission: NotificationPermission | null
error: Error | null
}

type NotificationAction = Partial<NotificationState>

type NotificationReducer = (
prevState: NotificationState,
action: NotificationAction,
) => NotificationState

/** Prevents updating state on an unmounted component. */
function useSafeDispatch(
dispatch: React.Dispatch<React.ReducerAction<NotificationReducer>>,
) {
const mounted = React.useRef(false)

useIsomorphicLayoutEffect(() => {
mounted.current = true

return () => {
mounted.current = false
}
}, [])

return React.useCallback(
(action: NotificationAction) => {
if (mounted.current) dispatch(action)
},
[dispatch],
)
}

interface UseNotificationReturnType {
/**
* Represents the permission status for displaying web notifications on
* the current origin.
*
* @example 'default' | 'denied' | 'granted'
*/
permission: NotificationPermission | null
/**
* Any error thrown while initializing the Notification constructor.
*/
error: Error | null
/**
* Requests permission to display web notifications.
*/
requestPermission: () => Promise<void>
/**
* Triggers a web notification.
*
* Note: The function signature matches the web Notification constructor one
* to one. See the MDN documentation for more information:
* https://developer.mozilla.org/en-US/docs/Web/API/Notification
*/
notify: (
title: Notification['title'],
options?: NotificationOptions,
) => Notification | null
}

export function useNotification(): UseNotificationReturnType {
const supported = isSupported()

const [{ permission, error }, setState] =
React.useReducer<NotificationReducer>(
(prevState, action) => ({ ...prevState, ...action }),
{
permission: supported ? Notification.permission : null,
error: !supported
? new Error('This browser does not support web notifications.')
: null,
},
)

const safeSetState = useSafeDispatch(setState)

const requestPermission = React.useCallback(async () => {
try {
if (!supported || permission !== 'default') return

const notificationPermission = await Notification.requestPermission()

// Update permission status and clear out any errors.
safeSetState({
permission: notificationPermission,
error: null,
})
} catch {
// Fallback to the deprecated callback API.
Notification.requestPermission((notificationPermission) => {
// Update permission status and clear out any errors.
safeSetState({
permission: notificationPermission,
error: null,
})
}).catch((deprecatedCallbackError) => {
if (deprecatedCallbackError instanceof Error) {
safeSetState({ error: deprecatedCallbackError })
}
})
}
}, [permission, supported])

const notify = React.useCallback(
(title: Notification['title'], options?: NotificationOptions) => {
if (!supported || permission !== 'granted') return null

try {
const notification = new Notification(title, options)

// Clear out possible errors in state.
safeSetState({ error: null })

return notification
} catch (notificationError) {
if (notificationError instanceof Error) {
safeSetState({ error: notificationError })
}

return null
}
},
[permission, supported],
)

return {
permission,
error,
requestPermission,
notify,
}
}
19 changes: 19 additions & 0 deletions hooks/use-notification/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from 'react'

export function isServer() {
const isDOM =
typeof window !== 'undefined' &&
window.document &&
window.document.documentElement

return !isDOM
}

/** Checks if Web Notification API supported. */
export function isSupported() {
return !isServer() && 'Notification' in window
}

export const useIsomorphicLayoutEffect = isServer()
? React.useEffect
: React.useLayoutEffect

3 comments on commit cde7145

@vercel
Copy link

@vercel vercel bot commented on cde7145 Aug 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

aacc-next-ts-styled-comps – ./recipes/next-ts-styled-comps

aacc-next-ts-styled-comps.vercel.app
aacc-next-ts-styled-comps-aaronccasanova.vercel.app
aacc-next-ts-styled-comps-git-main-aaronccasanova.vercel.app

@vercel
Copy link

@vercel vercel bot commented on cde7145 Aug 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

aacc-scales – ./apps/scales

aacc-scales-aaronccasanova.vercel.app
aacc-scales.vercel.app
aacc-scales-git-main-aaronccasanova.vercel.app

@vercel
Copy link

@vercel vercel bot commented on cde7145 Aug 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

aacc-next-ts – ./recipes/next-ts

aacc-next-ts.vercel.app
aacc-next-ts-aaronccasanova.vercel.app
aacc-next-ts-git-main-aaronccasanova.vercel.app

Please sign in to comment.