├── .npmignore
├── .babelrc
├── src
├── index.ts
├── app
│ ├── Notifications
│ │ ├── Message
│ │ │ ├── util.ts
│ │ │ ├── index.tsx
│ │ │ └── elements.ts
│ │ ├── elements.ts
│ │ └── index.tsx
│ ├── Button
│ │ ├── icons
│ │ │ ├── close.tsx
│ │ │ └── open.tsx
│ │ ├── index.tsx
│ │ └── elements.ts
│ ├── elements.ts
│ ├── app.tsx
│ ├── Embed
│ │ ├── elements.ts
│ │ └── index.tsx
│ └── index.tsx
├── types
│ ├── react.d.ts
│ ├── theme.d.ts
│ ├── store.d.ts
│ └── options.d.ts
├── api
│ ├── messages.ts
│ ├── types.d.ts
│ ├── embedAPI.ts
│ ├── renderer
│ │ ├── index.tsx
│ │ └── root.ts
│ ├── util.ts
│ └── index.ts
├── store
│ ├── actions
│ │ ├── constants.ts
│ │ └── index.ts
│ ├── defaultState.ts
│ └── index.ts
├── controllers
│ └── emotion
│ │ ├── emotion.ts
│ │ ├── index.tsx
│ │ └── types.d.ts
├── util
│ ├── cdn.ts
│ ├── compatibility.ts
│ ├── observe.ts
│ ├── parse.ts
│ └── validate
│ │ ├── validators.ts
│ │ └── index.ts
├── demo.html
└── umd.ts
├── .idea
├── misc.xml
├── .gitignore
├── modules.xml
├── vcs.xml
├── crate.iml
└── dbnavigator.xml
├── .gitignore
├── tsconfig.json
├── README.md
├── .github
└── ISSUE_TEMPLATE
│ └── config.yml
├── package.json
└── LICENSE.md
/.npmignore:
--------------------------------------------------------------------------------
1 | src
2 | .cache
3 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": ["emotion"]
3 | }
4 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export { default as cdn } from './util/cdn'
2 | export { default } from './api'
3 |
--------------------------------------------------------------------------------
/src/app/Notifications/Message/util.ts:
--------------------------------------------------------------------------------
1 | export const defaultAvatar = `https://cdn.discordapp.com/embed/avatars/0.png`
2 |
--------------------------------------------------------------------------------
/src/types/react.d.ts:
--------------------------------------------------------------------------------
1 | declare namespace JSX {
2 | interface IntrinsicElements {
3 | 'shadow-root': any
4 | 'shadow-styles': any
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/src/api/messages.ts:
--------------------------------------------------------------------------------
1 | enum Messages {
2 | EMBED_API_INVOCATION = 'Something went wrong! failed to connect to @widgetbot/embed-api!'
3 | }
4 |
5 | export default Messages
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Datasource local storage ignored files
5 | /dataSources/
6 | /dataSources.local.xml
7 | # Editor-based HTTP Client requests
8 | /httpRequests/
9 |
--------------------------------------------------------------------------------
/src/api/types.d.ts:
--------------------------------------------------------------------------------
1 | import { Client, IClient } from '@widgetbot/embed-api'
2 |
3 | export interface Events extends IClient.Events {}
4 |
5 | export type Event = keyof Events
6 |
7 | export type API = Client & {
8 | on: (event: T, callback: (data: Events[T]) => void) => void
9 | }
10 |
--------------------------------------------------------------------------------
/src/store/actions/constants.ts:
--------------------------------------------------------------------------------
1 | export const UPDATE_OPTIONS = 'UPDATE_OPTIONS'
2 | export const TOGGLE = 'TOGGLE'
3 | export const TOGGLE_VISIBILITY = 'TOGGLE_VISIBILITY'
4 | export const NOTIFICATION = 'NOTIFICATION'
5 | export const REMOVE_NOTIFICATION = 'REMOVE_NOTIFICATION'
6 | export const UPDATE_UNREAD_COUNT = 'UPDATE_UNREAD_COUNT'
7 |
--------------------------------------------------------------------------------
/src/store/defaultState.ts:
--------------------------------------------------------------------------------
1 | import Options from '../types/options'
2 | import { State } from '../types/store'
3 |
4 | const defaultState = (options: Options): State => ({
5 | options,
6 |
7 | interactive: !options.defer,
8 | visible: true,
9 | open: false,
10 |
11 | unread: 0,
12 | notifications: []
13 | })
14 |
15 | export default defaultState
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # dependencies
2 | /node_modules
3 |
4 | /coverage
5 | /umd
6 | /demo/dist
7 | /es
8 | /lib
9 | /dist
10 | /.vscode
11 | /.idea
12 |
13 | # misc
14 | .DS_Store
15 | .env.local
16 | .env.development.local
17 | .env.test.local
18 | .env.production.local
19 | .cache
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log
24 | debug.log
25 |
26 | .env
27 |
--------------------------------------------------------------------------------
/src/app/Button/icons/close.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | const Close = ({ className }: { className: string }) => (
4 |
10 | )
11 |
12 | export default Close
13 |
--------------------------------------------------------------------------------
/src/types/theme.d.ts:
--------------------------------------------------------------------------------
1 | import Options from './options'
2 |
3 | interface Theme {
4 | options: Options
5 | open: boolean
6 | visible: boolean
7 | coords: {
8 | x: {
9 | axis: 'left' | 'right'
10 | offset: number
11 | margin: number
12 | }
13 | y: {
14 | axis: 'top' | 'bottom'
15 | offset: number
16 | margin: number
17 | }
18 | }
19 | }
20 |
21 | export default Theme
22 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/controllers/emotion/emotion.ts:
--------------------------------------------------------------------------------
1 | import createEmotion from 'create-emotion'
2 |
3 | const create = (
4 | styleInjection: HTMLElement
5 | ): {
6 | flush
7 | hydrate
8 | cx
9 | merge
10 | getRegisteredStyles
11 | injectGlobal
12 | keyframes
13 | css
14 | sheet
15 | caches
16 | } => {
17 | const context = {}
18 |
19 | return createEmotion(context, {
20 | container: styleInjection
21 | })
22 | }
23 |
24 | export default create
25 |
--------------------------------------------------------------------------------
/src/types/store.d.ts:
--------------------------------------------------------------------------------
1 | import Options from './options'
2 |
3 | export interface Message {
4 | content: string
5 | id?: string
6 | avatar?: string
7 | visible?: boolean
8 | }
9 |
10 | export interface Notification extends Message {
11 | id: string;
12 | onClick?: () => void;
13 | }
14 |
15 | export interface State {
16 | options: Options
17 |
18 | interactive: boolean
19 | visible: boolean
20 | open: boolean
21 |
22 | unread: number
23 | notifications: Notification[]
24 | }
25 |
--------------------------------------------------------------------------------
/src/util/cdn.ts:
--------------------------------------------------------------------------------
1 | import Crate from '../api'
2 |
3 | const CDN_URL = `https://cdn.jsdelivr.net/npm/@widgetbot/crate@3`
4 |
5 | const loadFromCDN = () =>
6 | new Promise((resolve, reject) => {
7 | const script = document.createElement('script')
8 | script.src = CDN_URL
9 | document.head.appendChild(script)
10 |
11 | script.onload = () => resolve((window as any).Crate)
12 | script.onerror = () => reject('Failed to load Crate!')
13 | })
14 |
15 | export default loadFromCDN
16 |
--------------------------------------------------------------------------------
/src/app/elements.ts:
--------------------------------------------------------------------------------
1 | import ShadowStyles from '../controllers/emotion'
2 |
3 | export const Root = ShadowStyles(
4 | ({ styled }) => styled('div')`
5 | transition: opacity 0.2s ease;
6 | opacity: ${({ theme }) => (theme.visible ? 1 : 0)};
7 | pointer-events: ${({ theme }) => !theme.visible && 'none'};
8 |
9 | & :not(svg|*) {
10 | all: unset;
11 | }
12 |
13 | & * {
14 | box-sizing: border-box;
15 | -webkit-tap-highlight-color: transparent;
16 | }
17 | `
18 | )
19 |
--------------------------------------------------------------------------------
/src/util/compatibility.ts:
--------------------------------------------------------------------------------
1 | const nativeRef = () => {
2 | const iframe = document.createElement('iframe')
3 | iframe.setAttribute('style', 'display: none !important')
4 |
5 | document.documentElement.appendChild(iframe)
6 |
7 | return iframe.contentWindow as Window & { [key: string]: any }
8 | }
9 |
10 | // MooTools overwrites the .bind() method, this will restore it
11 | if ((window as any).MooTools) {
12 | const window = nativeRef()
13 | Function.prototype.bind = window.Function.prototype.bind
14 | }
15 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/crate.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/api/embedAPI.ts:
--------------------------------------------------------------------------------
1 | import Stylis from '@emotion/stylis'
2 | import { Store } from 'redux'
3 |
4 | import { State } from '../types/store'
5 | import { root } from './renderer'
6 | import { API } from './types'
7 |
8 | const { version } = require('../../package.json')
9 |
10 | export const stylis = new Stylis()
11 |
12 | class EmbedAPI {
13 | static stylis = stylis
14 |
15 | static version = version
16 | // Redux
17 | store: Store
18 |
19 | // Renderer
20 | static root = root
21 | node = root.createInstance()
22 |
23 | // API
24 | api: API
25 | on: API['on']
26 | emit: API['emit']
27 | }
28 |
29 | export default EmbedAPI
30 |
--------------------------------------------------------------------------------
/src/api/renderer/index.tsx:
--------------------------------------------------------------------------------
1 | import { Client } from '@widgetbot/embed-api'
2 | import * as React from 'react'
3 | import * as ReactDOM from 'react-dom'
4 | import { Provider } from 'react-redux'
5 |
6 | import App from '../../app'
7 | import { Node } from './root'
8 |
9 | interface Props {
10 | onAPI: (api: Client) => void
11 | node: Node
12 | store: any
13 | }
14 |
15 | const render = ({ node, store, ...props }: Props) => {
16 | ReactDOM.render(
17 |
18 |
19 | ,
20 | node
21 | )
22 |
23 | return node
24 | }
25 |
26 | export default render
27 |
28 | export { default as root } from './root'
29 | export * from './root'
30 |
--------------------------------------------------------------------------------
/src/api/renderer/root.ts:
--------------------------------------------------------------------------------
1 | export interface Root extends HTMLDivElement {
2 | createInstance(): HTMLDivElement
3 | }
4 |
5 | export type Node = HTMLDivElement
6 |
7 | const root = document.createElement('widgetbot-crate') as Root
8 |
9 | root.setAttribute('src', 'https://widgetbot.io')
10 | root.setAttribute('docs', 'docs.widgetbot.io')
11 |
12 | if (document.body) {
13 | document.body.appendChild(root)
14 | } else {
15 | document.addEventListener('DOMContentLoaded', () =>
16 | document.body.appendChild(root)
17 | )
18 | }
19 |
20 | root.createInstance = () => {
21 | const crate = document.createElement('crate') as Node
22 | root.appendChild(crate)
23 |
24 | return crate
25 | }
26 |
27 | export default root
28 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "dist",
4 | "module": "commonjs",
5 | "target": "es2017",
6 | "declaration": true,
7 | "lib": ["esnext", "es6", "dom"],
8 | "sourceMap": true,
9 | "skipLibCheck": true,
10 | "moduleResolution": "node",
11 | "jsx": "react",
12 | "experimentalDecorators": true,
13 | "allowSyntheticDefaultImports": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "esModuleInterop": true,
16 | "rootDir": "src",
17 | "baseUrl": "."
18 | },
19 | "exclude": [
20 | "node_modules",
21 | "build",
22 | "dist",
23 | "scripts",
24 | "acceptance-tests",
25 | "webpack",
26 | "jest",
27 | "src/setupTests.ts"
28 | ]
29 | }
30 |
--------------------------------------------------------------------------------
/src/api/util.ts:
--------------------------------------------------------------------------------
1 | import Options from '../types/options'
2 | import observe from '../util/observe'
3 |
4 | export const enhancer =
5 | (process.env.NODE_ENV === 'development' &&
6 | (window as any).__REDUX_DEVTOOLS_EXTENSION__ &&
7 | (window as any).__REDUX_DEVTOOLS_EXTENSION__()) ||
8 | undefined
9 |
10 | export const log = (method: keyof Console, ...data) =>
11 | console[method](
12 | '%c<{ crate.js }>',
13 | 'font-weight: bold; font-style: italic',
14 | ...data
15 | )
16 |
17 | export const observeOptions = (presets: Options, setter: Function) => {
18 | const store = {} as Options
19 | const options = observe(presets, store, (key, value) => {
20 | setter({ [key]: value })
21 | })
22 |
23 | return options
24 | }
25 |
--------------------------------------------------------------------------------
/src/util/observe.ts:
--------------------------------------------------------------------------------
1 | const observe = (
2 | value: T,
3 | store,
4 | callback: (key: Key, value: T[Key]) => void
5 | ): T & { get: () => T; set: (value: T) => T } => {
6 | let observed = {} as any
7 |
8 | Object.keys(value).forEach(key => {
9 | store[key] = value[key]
10 |
11 | Object.defineProperty(observed, key, {
12 | get: () => store[key],
13 | set(value) {
14 | callback(key as keyof T, value)
15 | store[key] = value
16 | }
17 | })
18 | })
19 |
20 | observed.get = () => store
21 | observed.set = value => {
22 | Object.keys(value).forEach(key => (store[key] = value[key]))
23 | return store
24 | }
25 |
26 | return observed
27 | }
28 |
29 | export default observe
30 |
--------------------------------------------------------------------------------
/src/app/Notifications/elements.ts:
--------------------------------------------------------------------------------
1 | import { TransitionGroup } from 'react-transition-group'
2 | import ShadowStyles from '../../controllers/emotion'
3 |
4 | export const Root = ShadowStyles(
5 | ({ styled, css }) => styled(TransitionGroup)`
6 | display: flex;
7 | flex-direction: ${({ theme }) =>
8 | theme.coords.y.axis === 'bottom' ? `column-reverse` : `column`};
9 |
10 | position: fixed;
11 | z-index: 2147482999;
12 | padding: 7px 0;
13 | width: 300px;
14 | max-height: calc(70% - 100px);
15 |
16 | ${({ theme }) => {
17 | const { x, y } = theme.coords
18 |
19 | return css({
20 | [x.axis]: x.offset,
21 | [y.axis]: y.offset + 56,
22 | [`padding-${y.axis}`]: '20px'
23 | })
24 | }};
25 | `
26 | )
27 |
--------------------------------------------------------------------------------
/src/util/parse.ts:
--------------------------------------------------------------------------------
1 | import Color from 'color'
2 |
3 | import Options from '../types/options'
4 |
5 | export const getCoords = (
6 | [y, x]: Options['location'],
7 | [xMargin, yMargin] = [20, 20]
8 | ) => ({
9 | x: {
10 | axis: typeof x === 'string' ? x : x > -1 ? 'left' : 'right',
11 | offset: typeof x === 'string' ? xMargin : Math.abs(x),
12 | margin: typeof x === 'string' ? xMargin : 0
13 | },
14 | y: {
15 | axis: typeof y === 'string' ? y : y > -1 ? 'top' : 'bottom',
16 | offset: typeof y === 'string' ? yMargin : Math.abs(y),
17 | margin: typeof y === 'string' ? yMargin : 0
18 | }
19 | })
20 |
21 | export const getAccent = (color: string): string =>
22 | (color => (color.luminosity() > 0.6 ? color.darken(0.7) : '#fff'))(
23 | Color(color)
24 | ).toString()
25 |
--------------------------------------------------------------------------------
/src/demo.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/src/store/actions/index.ts:
--------------------------------------------------------------------------------
1 | import { createAction } from 'redux-actions'
2 |
3 | import Options from '../../types/options'
4 | import { Notification } from '../../types/store'
5 | import {
6 | NOTIFICATION,
7 | REMOVE_NOTIFICATION,
8 | TOGGLE,
9 | TOGGLE_VISIBILITY,
10 | UPDATE_OPTIONS,
11 | UPDATE_UNREAD_COUNT
12 | } from './constants'
13 |
14 | export const updateOptions = createAction(UPDATE_OPTIONS)
15 |
16 | export const toggle = createAction(TOGGLE)
17 | export const toggleVisibility = createAction(TOGGLE_VISIBILITY)
18 |
19 | export const message = createAction(NOTIFICATION)
20 | export const deleteMessage = createAction<{ id: string; decrement?: boolean }>(
21 | REMOVE_NOTIFICATION
22 | )
23 |
24 | export const updateUnreadCount = createAction(UPDATE_UNREAD_COUNT)
25 |
--------------------------------------------------------------------------------
/src/app/Notifications/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { connect } from 'react-redux'
3 |
4 | import { Notification, State } from '../../types/store'
5 | import { Root } from './elements'
6 | import Message from './Message'
7 |
8 | interface StateProps {
9 | notifications: Notification[]
10 | }
11 |
12 | class Notifications extends React.PureComponent {
13 | render() {
14 | const messages = [...this.props.notifications].reverse()
15 |
16 | if (!messages.length) return null;
17 |
18 | return (
19 |
20 | {messages.map(message => message.onClick?.()} />)}
21 |
22 | )
23 | }
24 | }
25 |
26 | export default connect(({ notifications }) => ({
27 | notifications
28 | }))(Notifications)
29 |
--------------------------------------------------------------------------------
/src/app/Notifications/Message/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Transition } from 'react-transition-group'
3 |
4 | import { Notification } from '../../../types/store'
5 | import { Avatar, Content, Root } from './elements'
6 | import { defaultAvatar } from './util'
7 |
8 | class Message extends React.PureComponent void }> {
9 | render() {
10 | const { avatar, content, onClick } = this.props
11 |
12 | return (
13 |
14 | {state => (
15 | onClick?.()}>
16 |
17 |
18 |
19 | )}
20 |
21 | )
22 | }
23 | }
24 |
25 | export default Message
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Crate
2 |
3 | [](https://www.npmjs.com/package/@widgetbot/crate)
4 | [](https://docs.widgetbot.io/crate/)
5 | [](https://www.jsdelivr.com/package/npm/@widgetbot/crate)
6 |
7 | Clean & powerful popup Discord widgets for your website.
8 |
9 | 
10 |
11 | # Usage
12 |
13 | See the [documentation](https://docs.widgetbot.io/crate/) for more options.
14 |
15 | ```html
16 |
28 | ```
29 |
--------------------------------------------------------------------------------
/src/umd.ts:
--------------------------------------------------------------------------------
1 | import Crate from './api'
2 |
3 | console.log(
4 | `%c+%chttps://widgetbot.io\n%cPopup Discord chat widgets for your website.`,
5 | `font-size: 1px; margin-bottom: 5px; margin-left: 40px; padding: 10px 15px; line-height: 12px;background: url("https://i.imgur.com/S7IIIbE.png"); background-repeat: no-repeat; background-size: 30px; color: transparent;`,
6 | `padding-left: 2px; font-size: 14px; color: #7289DA; font-family: "Roboto", sans-serif`,
7 | `padding-left: 15px; font-size: 11px; font-family: "Roboto", sans-serif; `
8 | )
9 |
10 | if (window) {
11 | ;(window as any).Crate = Crate
12 |
13 | // Evaluate content inside