├── .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 | 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 | 5 | 9 | 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 | [![npm version](https://img.shields.io/npm/v/@widgetbot/crate.svg?style=flat-square)](https://www.npmjs.com/package/@widgetbot/crate) 4 | [![Docs](https://img.shields.io/badge/Docs-passing-green.svg?style=flat-square)](https://docs.widgetbot.io/crate/) 5 | [![JSDelivr](https://data.jsdelivr.com/v1/package/npm/@widgetbot/crate/badge)](https://www.jsdelivr.com/package/npm/@widgetbot/crate) 6 | 7 | Clean & powerful popup Discord widgets for your website. 8 | 9 | ![Demo](https://i.imgur.com/oq4W4Rk.gif) 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