├── .gitignore ├── .prettierrc ├── README.md ├── assets └── Preview.png ├── docs ├── layouts.md ├── logics.md ├── requests.md └── visuals.md ├── lerna.json ├── package.json ├── packages ├── client │ ├── README.md │ ├── assets │ │ ├── icon.icns │ │ └── icon.ico │ ├── package.json │ ├── public │ │ ├── app │ │ │ ├── auth.js │ │ │ ├── config.js │ │ │ ├── keyboard.js │ │ │ ├── menu.js │ │ │ └── shortcuts.js │ │ ├── assets │ │ │ ├── backgrounds │ │ │ │ └── Circle.png │ │ │ ├── electron │ │ │ │ ├── TrayTemplate.png │ │ │ │ └── TrayTemplate@2x.png │ │ │ ├── favicon │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ └── favicon.ico │ │ │ ├── features │ │ │ │ ├── Down.svg │ │ │ │ ├── Plus.svg │ │ │ │ └── Up.svg │ │ │ └── logo │ │ │ │ ├── Dark.png │ │ │ │ ├── Dark.svg │ │ │ │ ├── Light.png │ │ │ │ └── Standard.svg │ │ ├── electron.js │ │ ├── index.html │ │ └── manifest.json │ ├── src │ │ ├── clean.css │ │ ├── client.ts │ │ ├── components │ │ │ ├── buttons │ │ │ │ ├── Arrow.tsx │ │ │ │ ├── BadButton.tsx │ │ │ │ ├── Button.tsx │ │ │ │ ├── Circles.tsx │ │ │ │ ├── GoodButton.tsx │ │ │ │ └── MiniButton.tsx │ │ │ ├── cards │ │ │ │ ├── Card.tsx │ │ │ │ ├── Container.tsx │ │ │ │ ├── LightCard.tsx │ │ │ │ ├── Problems.tsx │ │ │ │ └── Result.tsx │ │ │ ├── editors │ │ │ │ ├── Editor.tsx │ │ │ │ ├── RegularEditor.tsx │ │ │ │ └── StatusEditor.tsx │ │ │ ├── forms │ │ │ │ ├── BundleForm.tsx │ │ │ │ ├── CardForm.tsx │ │ │ │ ├── CodeForm.tsx │ │ │ │ ├── LoginForm.tsx │ │ │ │ ├── PasswordForm.tsx │ │ │ │ ├── PreferencesForm.tsx │ │ │ │ ├── Problem.tsx │ │ │ │ ├── ProfileForm.tsx │ │ │ │ └── SignUpForm.tsx │ │ │ ├── inputs │ │ │ │ ├── CardInput.tsx │ │ │ │ ├── Control.tsx │ │ │ │ ├── LargeInput.tsx │ │ │ │ └── SimpleInput.tsx │ │ │ ├── layouts │ │ │ │ ├── Background.tsx │ │ │ │ ├── Cape.tsx │ │ │ │ ├── FormList.tsx │ │ │ │ ├── List.tsx │ │ │ │ ├── Marketplace.tsx │ │ │ │ ├── Modal.tsx │ │ │ │ ├── Onboard.tsx │ │ │ │ └── Split.tsx │ │ │ ├── lists │ │ │ │ ├── ChooseBundle.tsx │ │ │ │ └── ChooseCode.tsx │ │ │ ├── menus │ │ │ │ ├── CardMenu.tsx │ │ │ │ ├── ChooseAuth.tsx │ │ │ │ ├── Header.tsx │ │ │ │ └── PopupMenu.tsx │ │ │ ├── modals │ │ │ │ ├── BundleModal.tsx │ │ │ │ └── PreviewBundleModal.tsx │ │ │ ├── statefuls │ │ │ │ ├── Portal.tsx │ │ │ │ └── Toggle.tsx │ │ │ ├── texts │ │ │ │ ├── Subtitle.tsx │ │ │ │ └── Title.tsx │ │ │ └── toast │ │ │ │ ├── Container.js │ │ │ │ ├── Portal.js │ │ │ │ ├── Toast.js │ │ │ │ ├── Toaster.js │ │ │ │ ├── ToasterContext.js │ │ │ │ └── withToaster.js │ │ ├── config.ts │ │ ├── containers │ │ │ ├── App.tsx │ │ │ ├── Dashboard.tsx │ │ │ ├── menus │ │ │ │ ├── CodeMenu.tsx │ │ │ │ ├── SettingsMenu.tsx │ │ │ │ └── Topbar.tsx │ │ │ ├── modals │ │ │ │ ├── CreateBundle.tsx │ │ │ │ └── SelectBundle.tsx │ │ │ ├── pages │ │ │ │ ├── Auth.tsx │ │ │ │ ├── CreateCode.tsx │ │ │ │ ├── EditCode.tsx │ │ │ │ ├── ErrorCatch.tsx │ │ │ │ ├── FindCode.tsx │ │ │ │ ├── Login.tsx │ │ │ │ ├── Market.tsx │ │ │ │ ├── Settings.tsx │ │ │ │ └── SignUp.tsx │ │ │ ├── routes │ │ │ │ ├── AuthRoutes.tsx │ │ │ │ ├── MainRoutes.tsx │ │ │ │ └── SettingsRoutes.tsx │ │ │ └── settings │ │ │ │ ├── Accounts.tsx │ │ │ │ ├── Membership.tsx │ │ │ │ ├── Preferences.tsx │ │ │ │ ├── Profile.tsx │ │ │ │ └── Security.tsx │ │ ├── effects │ │ │ ├── useInstance.ts │ │ │ ├── useInstanceExecute.ts │ │ │ ├── useInstanceSuccess.ts │ │ │ └── useKeyboard.ts │ │ ├── index.css │ │ ├── index.tsx │ │ ├── react-app-env.d.ts │ │ ├── serviceWorker.ts │ │ ├── styles │ │ │ ├── animate.ts │ │ │ ├── bgs.ts │ │ │ ├── colors.ts │ │ │ ├── layouts.ts │ │ │ ├── shadows.ts │ │ │ ├── shapes.ts │ │ │ ├── states.ts │ │ │ └── words.ts │ │ └── utils │ │ │ ├── apolloPersistor.ts │ │ │ ├── assets.ts │ │ │ ├── authScope.ts │ │ │ ├── authStore.ts │ │ │ ├── components.ts │ │ │ ├── electron.ts │ │ │ ├── form.ts │ │ │ ├── intercom.ts │ │ │ ├── localPersistor.ts │ │ │ ├── record.ts │ │ │ └── toastStore.ts │ ├── tsconfig.json │ └── yarn.lock └── server │ ├── .env.example │ ├── .travis.yml │ ├── README.md │ ├── nodemon.json │ ├── now.production.json │ ├── package.json │ ├── src │ ├── config.ts │ ├── directives │ │ └── AuthDirective.ts │ ├── index.ts │ ├── models │ │ ├── Bundle.ts │ │ ├── Code.ts │ │ ├── Optin.ts │ │ ├── Provider.ts │ │ └── User.ts │ ├── resolvers │ │ ├── bundleResolvers.ts │ │ ├── codeResolvers.ts │ │ ├── optinResolvers.ts │ │ ├── providerResolvers.ts │ │ └── userResolvers.ts │ ├── schemas │ │ ├── @auth.gql │ │ ├── Bundle.gql │ │ ├── Code.gql │ │ ├── Optin.gql │ │ ├── Provider.gql │ │ ├── User.gql │ │ └── schema.gql │ └── utils │ │ ├── auth.ts │ │ ├── errors.ts │ │ ├── github.ts │ │ ├── models.ts │ │ ├── password.ts │ │ ├── payments.ts │ │ └── record.ts │ └── tsconfig.json ├── tslint.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | 4 | # testing 5 | coverage 6 | 7 | # production 8 | build 9 | dist 10 | lib 11 | 12 | # mac 13 | .DS_Store 14 | 15 | # Environment 16 | .env 17 | *.env* 18 | !.env.example 19 | 20 | # logs 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # lock file (we use yarn) 26 | package-lock.json 27 | 28 | # documentation 29 | .docz 30 | .cache 31 | 32 | # publishing 33 | electron-builder.yml -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Forge 2 | 3 | > 🏹 Free, unobtrusive, and modern tool for developers. 4 | 5 | Links to download a version can be found on [our website](https://useforge.co/). 6 | 7 | ![Forge](https://raw.githubusercontent.com/jackrobertscott/forge/master/assets/Preview.png) 8 | 9 | | Search by Name | Use ${1:Variables} | Edit in Seconds | 10 | |:---:|:---:|:---:| 11 | | ![Forge Screenshot](https://uploads-ssl.webflow.com/5be925d4130794d6c2052d79/5c133ce8d3261ab085c37be4_Main%20Bundle%20Snippets%20Menu.png) | ![Forge Screenshot](https://uploads-ssl.webflow.com/5be925d4130794d6c2052d79/5c133ce7ae722d326f9d7e37_Main%20Bundle%20Snippets%20Inserting.png) | ![Forge Screenshot](https://uploads-ssl.webflow.com/5be925d4130794d6c2052d79/5c133ce8ae722dd2099d7e38_Edit%20Snippet.png) | 12 | 13 | ## Overview 14 | 15 | Forge is a tool designed to aid 🏥 developers with basic development tasks such as creating and finding code snippets. 16 | 17 | ### Problems with current tools 18 | 19 | While designing Forge, we wanted to address the following difficulties faced when you only use a code editor while developing. 20 | 21 | 1. 🤔 Snippet shortcuts are hard to remember e.g. `rccp` or `conc`. 22 | 2. 😧 Creating custom snippets is really hard (usually you need to create an entire JSON page). 23 | 3. 😠 Settings don't save across your computers. 24 | 4. 😩 You can't preview a snippet before you start using it. 25 | 5. 😳 You can't search snippets by keywords. 26 | 27 | ### Features of Forge 28 | 29 | Forge was designed to overcome these issues by providing a developer tool which complements your code editor. 30 | 31 | 1. 🔥 Doesn't interrupt workflow; the app pops up above your editor and hides when you're done. 32 | 2. 😻 Easier to use; search snippets by full name *or* shortcut. 33 | 3. 💃 Preview your snippets before using them. 34 | 4. 🏆 Use `${1:variables}` inside your snippets in the same way you use VS Code snippets. 35 | 5. 🏎 Develop in-style with Forge's sleek and modern user interface. 36 | 37 | **Shortcut:** press `CmdOrCtrl+Shift+D` to toggle the Forge app's visibility. 38 | 39 | ## Technologies 40 | 41 | The Forge repository is a testing ground for a new sort of application design architecture. We have made Forge open source so that you can preview how we use and built this application using the lastest in app development technologies. 42 | 43 | ### Desktop 44 | 45 | The desktop application is built with [TypeScript](https://www.typescriptlang.org/) and is using: 46 | 47 | - [Electron](https://electronjs.org/): enables desktop applications to be built using web technologies. 48 | - [React](https://reactjs.org/): composes the interface layout and structure. 49 | - [lumbridge](https://github.com/jackrobertscott/lumbridge): manages application state and routing. 50 | - [monaco-editor](https://microsoft.github.io/monaco-editor/): the editor behind [Visual Studio Code](https://code.visualstudio.com/). 51 | 52 | ### Server 53 | 54 | The server is also built with [TypeScript](https://www.typescriptlang.org/) and is using: 55 | 56 | - [Node.js](https://nodejs.org/en/about/): enables JavaScript to be run as a server. 57 | - [Apollo GraphQL](https://www.apollographql.com/): creates a GraphQL interface for server data. 58 | - [MongoDB](https://www.mongodb.com/): a NoSQL database which works well with Node.js. 59 | - [mongoose](https://mongoosejs.com/): a schema validator used when working with MongoDB. 60 | 61 | ## Architecture 62 | 63 | There are very few good examples of good React application design and so we designed one. The front-end design system was the most challenging part. As such, we broke down the roles of the application into a specific modules. 64 | 65 | - [Requests](https://github.com/jackrobertscott/forge/blob/master/docs/requests.md): concerned with saving and retrieving data from persistent data sources. 66 | - [Logics](https://github.com/jackrobertscott/forge/blob/master/docs/logics.md): maps data from our requests to our graphical layouts. 67 | - [Layouts](https://github.com/jackrobertscott/forge/blob/master/docs/layouts.md): concerned with the structure and composition of the data and visual components. 68 | - [Visuals](https://github.com/jackrobertscott/forge/blob/master/docs/visuals.md): manages all the visuals on the page such as color, size, and spacing. 69 | 70 | ## Authors 71 | 72 | - Jack Scott [@jacrobsco](https://twitter.com/jacrobsco) - I tweet about coding and startups. 73 | -------------------------------------------------------------------------------- /assets/Preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackrobertscott/forge-181201/e7b47ef62a1a9fd6ea8c976bb7823dbbd6c735f3/assets/Preview.png -------------------------------------------------------------------------------- /docs/layouts.md: -------------------------------------------------------------------------------- 1 | # Layouts 2 | 3 | Determine the display arrangement. 4 | 5 | ```ts 6 | type ISnippetProps = DataComponent<{ 7 | snippet: IUserSnippetSingle 8 | }, { 9 | editSnippet: IEditSnippetHandler 10 | deleteSnippet: IDeleteSnippetHandler 11 | }>; 12 | 13 | /** 14 | * It's quite nice to destructure props on multiple lines like 15 | * this.. it makes it super easy to read. 16 | * 17 | * Also, don't destructure database object properties so that 18 | * it's clear what the data is related to when using it e.g. 19 | * No: const { id, contents } = snippet; doSomething(id, contents); 20 | * Yes: doSomething(snippet.id, snippet.contents); 21 | */ 22 | const Snippet: FunctionComponent = ({ 23 | data: { 24 | snippet, 25 | }, 26 | handlers: { 27 | editSnippet, 28 | deleteSnippet, 29 | } 30 | }) => { 31 | const handleEditSnippet = () => editSnippet(snippet); 32 | const handleDeleteSnippet = () => deleteSnippet(snippet); 33 | return ( 34 | 35 | 36 | {snippet.name} 37 | {snippet.contents} 38 | 39 | Edit 40 | 41 | 42 | Remove 43 | 44 | 45 | 46 | ); 47 | } 48 | 49 | type ISnippetListProps = DataComponent<{ 50 | snippets: IUserSnippets, 51 | }>; 52 | 53 | /** 54 | * This is concerned only about layout and consists of *no* 55 | * visuals or styling. 56 | */ 57 | export const SnippetList: FunctionComponent = ({ 58 | data: { 59 | snippets, 60 | }, 61 | handlers, 62 | }) => { 63 | return ( 64 | 65 | snippets.map(snippet => ( 66 | 70 | )); 71 | 72 | ); 73 | }; 74 | ``` 75 | 76 | ## Navigation 77 | 78 | - [Requests](https://github.com/jackrobertscott/forge/blob/master/docs/requests.md): concerned with saving and retrieving data from persistent data sources. 79 | - [Logics](https://github.com/jackrobertscott/forge/blob/master/docs/logics.md): maps data from our requests to our graphical layouts. 80 | - [Layouts](https://github.com/jackrobertscott/forge/blob/master/docs/layouts.md): concerned with the structure and composition of the data and visual components. 81 | - [Visuals](https://github.com/jackrobertscott/forge/blob/master/docs/visuals.md): manages all the visuals on the page such as color, size, and spacing. -------------------------------------------------------------------------------- /docs/logics.md: -------------------------------------------------------------------------------- 1 | # Logics 2 | 3 | Connect server requests with display. 4 | 5 | ```ts 6 | /** 7 | * Create a react component with no JSX. This is because 8 | * JSX instinctively implies "structure" in the code and 9 | * the logic handler should be completely unaware of the 10 | * structure of the app. Therefore JSX is not allowed in 11 | * the logic components. 12 | */ 13 | export const SnippetSearch = () => { 14 | usePersistorInstance({ 15 | instance: mutationDeleteSnippet, 16 | success: 'Snippet successfully deleted.', 17 | error: 'There was a problem deleteing the snippet.', 18 | }); 19 | const { 20 | status: { 21 | loading 22 | }, 23 | data: { 24 | userSnippets, 25 | }, 26 | } = usePersistorInstance({ 27 | instance: querySearchSnippets, 28 | execute: true, 29 | error: true, 30 | }); 31 | return React.createElement(UserSnippetList, { 32 | data: { 33 | snippets: userSnippets, 34 | loading: loading, 35 | }, 36 | handlers: { 37 | editSnippet, 38 | deleteSnippet, 39 | }, 40 | }); 41 | }; 42 | 43 | export type IEditSnippetHandler = ( 44 | snippet: IUserSnippetSingle 45 | ) => void; 46 | 47 | /** 48 | * Deal with the snippets as they are selected. 49 | */ 50 | const editSnippet: IEditSnippetHandler = ({ id }) => { 51 | Router.navigate(`/snippets/${id}/edit`); 52 | }; 53 | 54 | export type IDeleteSnippetHandler = ( 55 | snippet: IUserSnippetSingle 56 | ) => void; 57 | 58 | /** 59 | * These mutations are not related to layouts and therefore 60 | * they can be moved into a seperate file so that it's 61 | * easier for the bloody thing to be used... 62 | */ 63 | const deleteSnippet: IDeleteSnippetHandler = ({ id }) => { 64 | mutationDeleteSnippet.execute({ 65 | variables: id, 66 | }); 67 | }; 68 | ``` 69 | 70 | ## Navigation 71 | 72 | - [Requests](https://github.com/jackrobertscott/forge/blob/master/docs/requests.md): concerned with saving and retrieving data from persistent data sources. 73 | - [Logics](https://github.com/jackrobertscott/forge/blob/master/docs/logics.md): maps data from our requests to our graphical layouts. 74 | - [Layouts](https://github.com/jackrobertscott/forge/blob/master/docs/layouts.md): concerned with the structure and composition of the data and visual components. 75 | - [Visuals](https://github.com/jackrobertscott/forge/blob/master/docs/visuals.md): manages all the visuals on the page such as color, size, and spacing. -------------------------------------------------------------------------------- /docs/requests.md: -------------------------------------------------------------------------------- 1 | # Requests 2 | 3 | Declare data to be received from server. 4 | 5 | ```ts 6 | /** 7 | * Create an interface which will make it easier to use in 8 | * other component props and throughout our TS app. 9 | */ 10 | export interface IUserSnippetSingle { 11 | id: string; 12 | name: string; 13 | contents: string; 14 | language?: string; 15 | } 16 | export interface IUserSnippets { 17 | userSnippets: IUserSnippetSingle[]; 18 | } 19 | 20 | /** 21 | * Export the persistor instance so that other logic components 22 | * can refresh or re-execute it somewhere else in the code base. 23 | */ 24 | export const querySearchSnippets = apolloPersistor.on.query({ 25 | graphql: ` 26 | query UserSnippets { 27 | userSnippets { 28 | id 29 | name 30 | contents 31 | language 32 | } 33 | } 34 | `, 35 | }); 36 | ``` 37 | 38 | Declare mutations to be made on data. 39 | 40 | ```ts 41 | /** 42 | * Create an interface which will make it easier to use in 43 | * other component props and throughout our TS app. 44 | */ 45 | export interface IDeleteSnippet { 46 | deleteSnippet: { 47 | id?: string; 48 | }; 49 | } 50 | 51 | /** 52 | * Export the persistor instance so that other logic components 53 | * can refresh or re-execute it somewhere else in the code base. 54 | */ 55 | export const mutationDeleteSnippet = apolloPersistor.on.mutation({ 56 | graphql: ` 57 | mutation DeleteSnippet($id: String!) { 58 | deleteSnippet(id: $id) { 59 | id 60 | } 61 | } 62 | ` 63 | }); 64 | ``` 65 | 66 | ## Navigation 67 | 68 | - [Requests](https://github.com/jackrobertscott/forge/blob/master/docs/requests.md): concerned with saving and retrieving data from persistent data sources. 69 | - [Logics](https://github.com/jackrobertscott/forge/blob/master/docs/logics.md): maps data from our requests to our graphical layouts. 70 | - [Layouts](https://github.com/jackrobertscott/forge/blob/master/docs/layouts.md): concerned with the structure and composition of the data and visual components. 71 | - [Visuals](https://github.com/jackrobertscott/forge/blob/master/docs/visuals.md): manages all the visuals on the page such as color, size, and spacing. 72 | -------------------------------------------------------------------------------- /docs/visuals.md: -------------------------------------------------------------------------------- 1 | # Visuals 2 | 3 | Specify how the display will be visually styled. 4 | 5 | ```ts 6 | /** 7 | * This is using a little helper style library which provides 8 | * a number of valid properties and default values for a component. 9 | */ 10 | const Text = createStyle('div', { 11 | color: '#000', 12 | fontSize: '30px', 13 | }) 14 | .extra('danger', { 15 | color: 'red', 16 | '&:hover': { 17 | color: 'lightRed', 18 | }, 19 | }) 20 | .extra('big', { 21 | fontSize: '30px', 22 | backgroundColor: 'green', // Error: property not in defaults 23 | }); 24 | 25 | /** 26 | * Set the children specifically as a string to prevent 27 | * incorrect usage of the component. 28 | */ 29 | interface ITextProps { 30 | children?: string; 31 | [name: string]: any; 32 | } 33 | 34 | export default FunctionComponent = ({ 35 | children, 36 | ...args, 37 | }) => ( 38 | {children} 39 | ); 40 | ``` 41 | 42 | Currently, every element can be styled with the same CSS properties. Instead, we want each element to have a specific purpose and matching set of properties. 43 | 44 | - Text 45 | - Color 46 | - Font size 47 | - Text decoration 48 | - Text shadow 49 | - Input 50 | - Color 51 | - Line height 52 | - Font size 53 | - Rows 54 | - Shape 55 | - Height and width 56 | - Padding 57 | - Background color 58 | - Border and border radius 59 | - Layout 60 | - Flex properties 61 | - Margin between items 62 | 63 | The reason why we are splitting and limiting the responsibilities is because we want to reduce the amount of mental effort required to construct a layout. More ways of doing things leads to more mental effort to decide between options. Less ways of doing things ensures quick and consistent products. 64 | 65 | ## Navigation 66 | 67 | - [Requests](https://github.com/jackrobertscott/forge/blob/master/docs/requests.md): concerned with saving and retrieving data from persistent data sources. 68 | - [Logics](https://github.com/jackrobertscott/forge/blob/master/docs/logics.md): maps data from our requests to our graphical layouts. 69 | - [Layouts](https://github.com/jackrobertscott/forge/blob/master/docs/layouts.md): concerned with the structure and composition of the data and visual components. 70 | - [Visuals](https://github.com/jackrobertscott/forge/blob/master/docs/visuals.md): manages all the visuals on the page such as color, size, and spacing. -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.3.2", 3 | "npmClient": "yarn", 4 | "useWorkspaces": true, 5 | "packages": [ 6 | "packages/*" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "workspaces": [ 5 | "packages/*" 6 | ], 7 | "scripts": { 8 | "bootstrap": "lerna bootstrap", 9 | "test": "lerna run test", 10 | "build": "lerna run build", 11 | "ship": "lerna publish", 12 | "prepublishOnly": "yarn run build", 13 | "launch": "cd examples && yarn start && cd .." 14 | }, 15 | "devDependencies": { 16 | "lerna": "^3.5.0", 17 | "prettier": "^1.15.2", 18 | "tslint": "^5.11.0", 19 | "tslint-config-prettier": "^1.16.0", 20 | "tslint-eslint-rules": "^5.4.0", 21 | "tslint-react": "^3.6.0", 22 | "typescript": "^3.1.6" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/client/README.md: -------------------------------------------------------------------------------- 1 | # Forge Client 2 | 3 | > 🏹 Unobtrusive and beautiful desktop app built to improve the development experience. 4 | 5 | This is the desktop app code for the Forge app. -------------------------------------------------------------------------------- /packages/client/assets/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackrobertscott/forge-181201/e7b47ef62a1a9fd6ea8c976bb7823dbbd6c735f3/packages/client/assets/icon.icns -------------------------------------------------------------------------------- /packages/client/assets/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackrobertscott/forge-181201/e7b47ef62a1a9fd6ea8c976bb7823dbbd6c735f3/packages/client/assets/icon.ico -------------------------------------------------------------------------------- /packages/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "forge", 3 | "version": "0.3.3", 4 | "private": true, 5 | "description": "Unobtrusive and beautiful desktop app built to improve the development experience.", 6 | "main": "public/electron.js", 7 | "homepage": "./", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/jackrobertscott/forge.git" 11 | }, 12 | "author": { 13 | "name": "Jack Scott", 14 | "email": "jack.rob.scott@gmail.com" 15 | }, 16 | "scripts": { 17 | "dev": "react-scripts start", 18 | "test": "react-scripts test", 19 | "open": "yarn link lumbridge && yarn link dragonfire", 20 | "start": "concurrently \"BROWSER=none yarn run dev\" \"yarn run electron\"", 21 | "start:win": "concurrently \"SET BROWSER=none&&yarn run dev\" \"yarn run electron\"", 22 | "electron": "wait-on http://localhost:3000 && electron .", 23 | "ship": "yarn run clean && yarn run build:react && yarn run build:electron --mac --linux --win --publish=always", 24 | "build": "yarn run ship", 25 | "build:test": "yarn run clean && yarn run build:react && yarn run build:electron --dir", 26 | "build:react": "REACT_APP_NODE_ENV=production react-scripts build", 27 | "build:electron": "electron-builder --config=electron-builder.yml", 28 | "build:storybook": "build-storybook -s public", 29 | "storybook": "start-storybook -p 9009 -s public", 30 | "clean": "rm -rf build dist" 31 | }, 32 | "dependencies": { 33 | "@sentry/browser": "^4.4.1", 34 | "@sentry/electron": "^0.14.0", 35 | "@types/change-case": "^2.3.1", 36 | "@types/color": "^3.0.0", 37 | "@types/electron-store": "^1.3.0", 38 | "@types/node": "10.12.11", 39 | "@types/react": "16.7.11", 40 | "@types/react-dom": "16.0.11", 41 | "@types/react-outside-click-handler": "^1.2.0", 42 | "@types/react-stripe-elements": "^1.1.9", 43 | "@types/styled-components": "^4.1.3", 44 | "@types/throttle-debounce": "^1.1.0", 45 | "@types/yup": "^0.26.3", 46 | "analytics-node": "^3.3.0", 47 | "apollo-boost": "^0.1.22", 48 | "change-case": "^3.0.2", 49 | "color": "^3.1.0", 50 | "dragonfire": "^0.0.1", 51 | "electron-is-dev": "^1.0.1", 52 | "electron-store": "^2.0.0", 53 | "electron-updater": "^4.0.6", 54 | "formik": "^1.3.2", 55 | "graphql-tag": "^2.10.0", 56 | "keycode": "^2.2.0", 57 | "lumbridge": "^0.1.1", 58 | "monaco-editor": "^0.15.6", 59 | "query-string": "^6.2.0", 60 | "react": "^16.7.0-alpha.2", 61 | "react-dom": "^16.7.0-alpha.2", 62 | "react-outside-click-handler": "^1.2.2", 63 | "react-stripe-elements": "^2.0.1", 64 | "styled-components": "^4.1.2", 65 | "throttle-debounce": "^2.0.1", 66 | "yup": "^0.26.6" 67 | }, 68 | "devDependencies": { 69 | "@types/jest": "23.3.10", 70 | "concurrently": "^4.1.0", 71 | "electron": "^3.0.12", 72 | "electron-builder": "^20.38.3", 73 | "react-scripts": "2.1.1", 74 | "typescript": "3.2.1", 75 | "wait-on": "^3.2.0" 76 | }, 77 | "eslintConfig": { 78 | "extends": "react-app" 79 | }, 80 | "browserslist": [ 81 | ">0.2%", 82 | "not dead", 83 | "not ie <= 11", 84 | "not op_mini all" 85 | ] 86 | } 87 | -------------------------------------------------------------------------------- /packages/client/public/app/auth.js: -------------------------------------------------------------------------------- 1 | const electron = require('electron'); 2 | const queryString = require('query-string'); 3 | 4 | const { ipcMain, BrowserWindow } = electron; 5 | 6 | /** 7 | * Create a window to load github auth. 8 | */ 9 | function createAuthWindow(authUrl) { 10 | const window = new BrowserWindow({ 11 | width: 800, 12 | height: 600, 13 | backgroundColor: '#fafbfc', 14 | titleBarStyle: 'hiddenInset', 15 | show: false, 16 | }); 17 | window.loadURL(authUrl); 18 | window.show(); 19 | return window; 20 | } 21 | 22 | /** 23 | * Handle the changed url. 24 | */ 25 | function handleCallback(url, authWindow, senderWindow) { 26 | if ( 27 | url.startsWith('https://useforge.co') || 28 | url.startsWith('http://localhost:3000') 29 | ) { 30 | const { query: { code, error } = {} } = queryString.parseUrl(url); 31 | if (code) { 32 | senderWindow.send('authGitHubCode', code); 33 | } 34 | if (error) { 35 | senderWindow.send('authGitHubError', error); 36 | } 37 | authWindow.destroy(); 38 | } 39 | } 40 | 41 | /** 42 | * This listens for a "dismiss" event sent from the app. 43 | */ 44 | ipcMain.on('authGitHub', ({ sender }, authUrl) => { 45 | const senderWindow = sender; 46 | const authWindow = createAuthWindow(authUrl); 47 | authWindow.webContents.on('will-navigate', (_, url) => 48 | handleCallback(url, authWindow, senderWindow) 49 | ); 50 | authWindow.webContents.session.webRequest.onBeforeRedirect( 51 | ({ redirectURL }) => handleCallback(redirectURL, authWindow, senderWindow) 52 | ); 53 | }); 54 | -------------------------------------------------------------------------------- /packages/client/public/app/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | openOnStart: false, // app will show everytime computer restarts if true 3 | hideDockOnStart: true, // opening dock prevents opening over other screens 4 | }; 5 | -------------------------------------------------------------------------------- /packages/client/public/app/keyboard.js: -------------------------------------------------------------------------------- 1 | const { Menu } = require('electron'); 2 | 3 | module.exports = () => { 4 | if (Menu.getApplicationMenu()) return; 5 | const template = [ 6 | { 7 | label: 'Edit', 8 | submenu: [ 9 | { 10 | role: 'undo', 11 | }, 12 | { 13 | role: 'redo', 14 | }, 15 | { 16 | type: 'separator', 17 | }, 18 | { 19 | role: 'cut', 20 | }, 21 | { 22 | role: 'copy', 23 | }, 24 | { 25 | role: 'paste', 26 | }, 27 | { 28 | role: 'pasteandmatchstyle', 29 | }, 30 | { 31 | role: 'delete', 32 | }, 33 | { 34 | role: 'selectall', 35 | }, 36 | ], 37 | }, 38 | { 39 | role: 'window', 40 | submenu: [ 41 | { 42 | role: 'minimize', 43 | }, 44 | { 45 | role: 'close', 46 | }, 47 | ], 48 | }, 49 | ]; 50 | const menu = Menu.buildFromTemplate(template); 51 | Menu.setApplicationMenu(menu); 52 | }; 53 | -------------------------------------------------------------------------------- /packages/client/public/app/menu.js: -------------------------------------------------------------------------------- 1 | module.exports.createItems = ({ app, getWindow, showWindow }) => [ 2 | { 3 | label: 'Open', 4 | accelerator: 'CmdOrCtrl+Shift+D', 5 | click: showWindow, 6 | }, 7 | { 8 | type: 'separator', 9 | }, 10 | { 11 | label: 'Inspect', 12 | accelerator: 'CmdOrCtrl+Alt+I', 13 | click: () => { 14 | const window = getWindow(); 15 | if (window) { 16 | window.toggleDevTools(); 17 | } 18 | }, 19 | }, 20 | { 21 | label: 'Reload', 22 | accelerator: 'CmdOrCtrl+R', 23 | click: () => { 24 | app.relaunch(); 25 | app.exit(); 26 | }, 27 | }, 28 | { 29 | label: 'Quit', 30 | accelerator: 'CmdOrCtrl+Q', 31 | click: () => { 32 | app.exit(); 33 | }, 34 | }, 35 | ]; 36 | -------------------------------------------------------------------------------- /packages/client/public/app/shortcuts.js: -------------------------------------------------------------------------------- 1 | const electron = require('electron'); 2 | 3 | const { globalShortcut, ipcMain } = electron; 4 | 5 | /** 6 | * Note that these "global" shortcuts are global 7 | * and will overwrite other shortcuts. 8 | */ 9 | module.exports = ({ createOrFocusWindow }) => { 10 | const defaultShortcuts = { 11 | open: 'CmdOrCtrl+Shift+D', 12 | }; 13 | const shortcuts = Object.assign({}, defaultShortcuts); 14 | function updateShortcuts() { 15 | globalShortcut.unregisterAll(); 16 | globalShortcut.register( 17 | shortcuts.open || defaultShortcuts.open, 18 | createOrFocusWindow 19 | ); 20 | } 21 | ipcMain.on('updateShortcuts', (_, updates) => { 22 | Object.assign(shortcuts, updates || {}); 23 | updateShortcuts(); 24 | }); 25 | updateShortcuts(); 26 | }; 27 | -------------------------------------------------------------------------------- /packages/client/public/assets/backgrounds/Circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackrobertscott/forge-181201/e7b47ef62a1a9fd6ea8c976bb7823dbbd6c735f3/packages/client/public/assets/backgrounds/Circle.png -------------------------------------------------------------------------------- /packages/client/public/assets/electron/TrayTemplate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackrobertscott/forge-181201/e7b47ef62a1a9fd6ea8c976bb7823dbbd6c735f3/packages/client/public/assets/electron/TrayTemplate.png -------------------------------------------------------------------------------- /packages/client/public/assets/electron/TrayTemplate@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackrobertscott/forge-181201/e7b47ef62a1a9fd6ea8c976bb7823dbbd6c735f3/packages/client/public/assets/electron/TrayTemplate@2x.png -------------------------------------------------------------------------------- /packages/client/public/assets/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackrobertscott/forge-181201/e7b47ef62a1a9fd6ea8c976bb7823dbbd6c735f3/packages/client/public/assets/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /packages/client/public/assets/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackrobertscott/forge-181201/e7b47ef62a1a9fd6ea8c976bb7823dbbd6c735f3/packages/client/public/assets/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /packages/client/public/assets/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackrobertscott/forge-181201/e7b47ef62a1a9fd6ea8c976bb7823dbbd6c735f3/packages/client/public/assets/favicon/favicon.ico -------------------------------------------------------------------------------- /packages/client/public/assets/features/Down.svg: -------------------------------------------------------------------------------- 1 | Down -------------------------------------------------------------------------------- /packages/client/public/assets/features/Plus.svg: -------------------------------------------------------------------------------- 1 | Plus_1 -------------------------------------------------------------------------------- /packages/client/public/assets/features/Up.svg: -------------------------------------------------------------------------------- 1 | Up -------------------------------------------------------------------------------- /packages/client/public/assets/logo/Dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackrobertscott/forge-181201/e7b47ef62a1a9fd6ea8c976bb7823dbbd6c735f3/packages/client/public/assets/logo/Dark.png -------------------------------------------------------------------------------- /packages/client/public/assets/logo/Dark.svg: -------------------------------------------------------------------------------- 1 | Standard -------------------------------------------------------------------------------- /packages/client/public/assets/logo/Light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackrobertscott/forge-181201/e7b47ef62a1a9fd6ea8c976bb7823dbbd6c735f3/packages/client/public/assets/logo/Light.png -------------------------------------------------------------------------------- /packages/client/public/assets/logo/Standard.svg: -------------------------------------------------------------------------------- 1 | Standard -------------------------------------------------------------------------------- /packages/client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | Forge 24 | 43 | 44 | 49 | 50 | 51 | 52 | 53 | 54 | 56 | 57 | 60 | 61 |
62 |
63 | 64 |
65 |
66 | 67 | 68 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /packages/client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Forge", 3 | "name": "Coding evolved.", 4 | "icons": [ 5 | { 6 | "src": "assets/favicon/favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#1F1F1F", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /packages/client/src/clean.css: -------------------------------------------------------------------------------- 1 | *[class^="sc-"], *[class*=" sc-"] { 2 | animation: none; 3 | animation-delay: 0; 4 | animation-direction: normal; 5 | animation-duration: 0; 6 | animation-fill-mode: none; 7 | animation-iteration-count: 1; 8 | animation-name: none; 9 | animation-play-state: running; 10 | animation-timing-function: ease; 11 | backface-visibility: visible; 12 | background: 0; 13 | background-attachment: scroll; 14 | background-clip: border-box; 15 | background-color: transparent; 16 | background-image: none; 17 | background-origin: padding-box; 18 | background-position: 0 0; 19 | background-position-x: 0; 20 | background-position-y: 0; 21 | background-repeat: repeat; 22 | background-size: auto auto; 23 | border: 0; 24 | border-style: none; 25 | border-width: medium; 26 | border-color: inherit; 27 | border-bottom: 0; 28 | border-bottom-color: inherit; 29 | border-bottom-left-radius: 0; 30 | border-bottom-right-radius: 0; 31 | border-bottom-style: none; 32 | border-bottom-width: medium; 33 | border-collapse: separate; 34 | border-image: none; 35 | border-left: 0; 36 | border-left-color: inherit; 37 | border-left-style: none; 38 | border-left-width: medium; 39 | border-radius: 0; 40 | border-right: 0; 41 | border-right-color: inherit; 42 | border-right-style: none; 43 | border-right-width: medium; 44 | border-spacing: 0; 45 | border-top: 0; 46 | border-top-color: inherit; 47 | border-top-left-radius: 0; 48 | border-top-right-radius: 0; 49 | border-top-style: none; 50 | border-top-width: medium; 51 | bottom: auto; 52 | box-shadow: none; 53 | box-sizing: border-box; 54 | caption-side: top; 55 | clear: none; 56 | clip: auto; 57 | color: inherit; 58 | columns: auto; 59 | column-count: auto; 60 | column-fill: balance; 61 | column-gap: normal; 62 | column-rule: medium none currentColor; 63 | column-rule-color: currentColor; 64 | column-rule-style: none; 65 | column-rule-width: none; 66 | column-span: 1; 67 | column-width: auto; 68 | content: normal; 69 | counter-increment: none; 70 | counter-reset: none; 71 | cursor: inherit; 72 | direction: ltr; 73 | display: block; 74 | empty-cells: show; 75 | float: none; 76 | font: normal; 77 | font-family: inherit; 78 | font-size: medium; 79 | font-style: normal; 80 | font-variant: normal; 81 | font-weight: normal; 82 | height: auto; 83 | hyphens: none; 84 | left: auto; 85 | letter-spacing: normal; 86 | line-height: normal; 87 | list-style: none; 88 | list-style-image: none; 89 | list-style-position: outside; 90 | list-style-type: disc; 91 | margin: 0; 92 | margin-bottom: 0; 93 | margin-left: 0; 94 | margin-right: 0; 95 | margin-top: 0; 96 | max-height: none; 97 | max-width: none; 98 | min-height: 0; 99 | min-width: 0; 100 | opacity: 1; 101 | orphans: 0; 102 | outline: 0; 103 | outline-color: invert; 104 | outline-style: none; 105 | outline-width: medium; 106 | overflow: visible; 107 | overflow-x: visible; 108 | overflow-y: visible; 109 | padding: 0; 110 | padding-bottom: 0; 111 | padding-left: 0; 112 | padding-right: 0; 113 | padding-top: 0; 114 | page-break-after: auto; 115 | page-break-before: auto; 116 | page-break-inside: auto; 117 | perspective: none; 118 | perspective-origin: 50% 50%; 119 | position: static; 120 | right: auto; 121 | tab-size: 8; 122 | table-layout: auto; 123 | text-align: inherit; 124 | text-align-last: auto; 125 | text-decoration: none; 126 | text-decoration-color: inherit; 127 | text-decoration-line: none; 128 | text-decoration-style: solid; 129 | text-indent: 0; 130 | text-shadow: none; 131 | text-transform: none; 132 | top: auto; 133 | transform: none; 134 | transform-style: flat; 135 | transition: none; 136 | transition-delay: 0s; 137 | transition-duration: 0s; 138 | transition-property: none; 139 | transition-timing-function: ease; 140 | unicode-bidi: normal; 141 | visibility: visible; 142 | white-space: normal; 143 | widows: 0; 144 | width: auto; 145 | word-spacing: normal; 146 | z-index: auto; 147 | } -------------------------------------------------------------------------------- /packages/client/src/client.ts: -------------------------------------------------------------------------------- 1 | import ApolloClient, { Operation } from 'apollo-boost'; 2 | import config from './config'; 3 | import authStore from './utils/authStore'; 4 | 5 | const authenticateRequests = (operation: Operation) => { 6 | const token = authStore.state.token; 7 | if (token) { 8 | operation.setContext({ 9 | headers: { 10 | authorization: token, 11 | }, 12 | }); 13 | } 14 | }; 15 | 16 | export default new ApolloClient({ 17 | uri: config.urls.api, 18 | request: authenticateRequests as () => any, 19 | }); 20 | -------------------------------------------------------------------------------- /packages/client/src/components/buttons/Arrow.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { loadAsset } from '../../utils/assets'; 3 | 4 | export default styled('img').attrs({ src: loadAsset('features/Up.svg') })` 5 | height: 1em; 6 | transform: rotate(90deg); 7 | filter: invert(100%); 8 | opacity: 0.9; 9 | `; 10 | -------------------------------------------------------------------------------- /packages/client/src/components/buttons/BadButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode, FunctionComponent } from 'react'; 2 | import styled, { css } from 'styled-components'; 3 | import bgs from '../../styles/bgs'; 4 | import shapes from '../../styles/shapes'; 5 | import shadows from '../../styles/shadows'; 6 | import states from '../../styles/states'; 7 | import layouts from '../../styles/layouts'; 8 | 9 | const Wrap = styled('button').attrs({ type: 'button' })` 10 | ${bgs.danger} 11 | ${shapes.narrow} 12 | ${shadows.simple} 13 | ${layouts.space} 14 | ${layouts.noshrink} 15 | ${layouts.rowsCenter} 16 | justify-content: space-between; 17 | ${states.hovered(bgs.dangerLight)} 18 | ${states.clicked([bgs.danger, shadows.none])} 19 | ${({ auto }: { auto?: string; [name: string]: any }) => 20 | auto && 21 | css` 22 | margin-${auto}: auto; 23 | `} 24 | `; 25 | 26 | export interface IBadButton { 27 | children: ReactNode; 28 | loading?: boolean; 29 | icon?: ReactNode; 30 | [property: string]: any; 31 | } 32 | 33 | const BadButton: FunctionComponent = ({ 34 | children, 35 | loading, 36 | icon, 37 | ...args 38 | }) => ( 39 | 40 | {loading ? 'Loading...' : children} 41 | {icon} 42 | 43 | ); 44 | 45 | export default BadButton; 46 | -------------------------------------------------------------------------------- /packages/client/src/components/buttons/Button.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode, FunctionComponent } from 'react'; 2 | import styled, { css } from 'styled-components'; 3 | import bgs from '../../styles/bgs'; 4 | import shapes from '../../styles/shapes'; 5 | import shadows from '../../styles/shadows'; 6 | import states from '../../styles/states'; 7 | import layouts from '../../styles/layouts'; 8 | 9 | const Wrap = styled('button').attrs({ type: 'button' })` 10 | ${bgs.dark} 11 | ${shapes.narrow} 12 | ${shadows.simple} 13 | ${layouts.space} 14 | ${layouts.noshrink} 15 | ${layouts.rowsCenter} 16 | justify-content: space-between; 17 | ${states.hovered(bgs.darkLight)} 18 | ${states.clicked([bgs.dark, shadows.none])} 19 | ${({ auto }: { auto?: string; [name: string]: any }) => 20 | auto && 21 | css` 22 | margin-${auto}: auto; 23 | `} 24 | `; 25 | 26 | export interface IButton { 27 | children: ReactNode; 28 | loading?: boolean; 29 | icon?: ReactNode; 30 | [property: string]: any; 31 | } 32 | 33 | const Button: FunctionComponent = ({ 34 | children, 35 | loading, 36 | icon, 37 | ...args 38 | }) => ( 39 | 40 | {loading ? 'Loading...' : children} 41 | {icon} 42 | 43 | ); 44 | 45 | export default Button; 46 | -------------------------------------------------------------------------------- /packages/client/src/components/buttons/Circles.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled, { css } from 'styled-components'; 3 | import colors from '../../styles/colors'; 4 | import layouts from '../../styles/layouts'; 5 | 6 | const Wrap = styled('div')` 7 | ${layouts.rowsCenter} 8 | height: ${({ height }: { height?: string; [name: string]: any }) => 9 | height || '15px'}; 10 | `; 11 | 12 | const Circle = styled('div')` 13 | background-color: ${colors.white}; 14 | height: 6px; 15 | width: 6px; 16 | border-radius: 50%; 17 | margin-right: 3px; 18 | &:last-child { 19 | margin-right: 0; 20 | } 21 | `; 22 | 23 | export default (args: any) => ( 24 | 25 | 26 | 27 | 28 | 29 | ); 30 | -------------------------------------------------------------------------------- /packages/client/src/components/buttons/GoodButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode, FunctionComponent } from 'react'; 2 | import styled, { css } from 'styled-components'; 3 | import bgs from '../../styles/bgs'; 4 | import shapes from '../../styles/shapes'; 5 | import shadows from '../../styles/shadows'; 6 | import states from '../../styles/states'; 7 | import layouts from '../../styles/layouts'; 8 | import colors from '../../styles/colors'; 9 | 10 | const Wrap = styled('button').attrs({ type: 'button' })` 11 | ${bgs.marine} 12 | ${shapes.narrow} 13 | ${shadows.simple} 14 | ${layouts.space} 15 | ${layouts.noshrink} 16 | ${layouts.rowsCenter} 17 | justify-content: space-between; 18 | ${states.hovered(bgs.marineLight)} 19 | ${states.clicked([bgs.marine, shadows.none])} 20 | ${({ auto }: { auto?: string; [name: string]: any }) => 21 | auto && 22 | css` 23 | margin-${auto}: auto; 24 | `} 25 | ${({ bright }: { bright?: string | boolean; [name: string]: any }) => 26 | bright && 27 | css` 28 | color: ${colors.white}; 29 | &:hover { 30 | color: ${colors.white}; 31 | } 32 | `} 33 | `; 34 | 35 | export interface IGoodButton { 36 | children: ReactNode; 37 | loading?: boolean; 38 | icon?: ReactNode; 39 | [property: string]: any; 40 | } 41 | 42 | const GoodButton: FunctionComponent = ({ 43 | children, 44 | loading, 45 | icon, 46 | ...args 47 | }) => ( 48 | 49 | {loading ? 'Loading...' : children} 50 | {icon} 51 | 52 | ); 53 | 54 | export default GoodButton; 55 | -------------------------------------------------------------------------------- /packages/client/src/components/buttons/MiniButton.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import bgs from '../../styles/bgs'; 3 | import shadows from '../../styles/shadows'; 4 | import shapes from '../../styles/shapes'; 5 | import words from '../../styles/words'; 6 | import states from '../../styles/states'; 7 | 8 | export default styled('button')` 9 | ${({ ...args }: { to?: boolean | string; [name: string]: any }) => null} 10 | ${shapes.mini} 11 | ${shadows.simple} 12 | ${bgs.darkLight} 13 | ${states.hovered(bgs.darkLighter)} 14 | ${states.clicked([bgs.darkLight, shadows.none])} 15 | ${words.small} 16 | flex-shrink: 0; 17 | margin-left: 10px; 18 | cursor: pointer; 19 | `; 20 | -------------------------------------------------------------------------------- /packages/client/src/components/cards/Card.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import bgs from '../../styles/bgs'; 3 | import shapes from '../../styles/shapes'; 4 | import shadows from '../../styles/shadows'; 5 | import layouts from '../../styles/layouts'; 6 | 7 | export default styled('div')` 8 | ${bgs.dark} 9 | ${shapes.simple} 10 | ${shadows.simple} 11 | ${layouts.space} 12 | ${({ slim }: any) => slim && shapes.thin} 13 | flex-grow: 1; 14 | flex-shrink: 0; 15 | `; 16 | -------------------------------------------------------------------------------- /packages/client/src/components/cards/Container.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export default styled('div')` 4 | width: 240px; 5 | max-width: 240px; 6 | flex-grow: 1; 7 | flex-shrink: 0; 8 | margin: 15px; 9 | `; 10 | -------------------------------------------------------------------------------- /packages/client/src/components/cards/LightCard.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import bgs from '../../styles/bgs'; 3 | import shapes from '../../styles/shapes'; 4 | import shadows from '../../styles/shadows'; 5 | import layouts from '../../styles/layouts'; 6 | 7 | export default styled('div')` 8 | ${bgs.darkLight} 9 | ${shapes.simple} 10 | ${shadows.simple} 11 | ${layouts.space} 12 | ${({ slim }: any) => slim && shapes.thin} 13 | flex-grow: 1; 14 | flex-shrink: 0; 15 | `; 16 | -------------------------------------------------------------------------------- /packages/client/src/components/cards/Problems.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react'; 2 | import styled from 'styled-components'; 3 | import bgs from '../../styles/bgs'; 4 | import shapes from '../../styles/shapes'; 5 | import shadows from '../../styles/shadows'; 6 | import layouts from '../../styles/layouts'; 7 | import { sentenceCase } from 'change-case'; 8 | 9 | const Wrap = styled('div')` 10 | ${bgs.danger} 11 | ${shapes.simple} 12 | ${shadows.simple} 13 | ${layouts.space} 14 | ${({ slim }: any) => slim && shapes.thin} 15 | flex-grow: 1; 16 | flex-shrink: 0; 17 | `; 18 | 19 | export interface IProblemProps { 20 | items?: { 21 | [error: string]: string; 22 | }; 23 | [name: string]: any; 24 | } 25 | 26 | const Problems: FunctionComponent = ({ 27 | items: errors = {}, 28 | ...args 29 | }) => { 30 | const mapErrors = Object.keys(errors).map(key => ( 31 |
{sentenceCase((errors as any)[key])}
32 | )); 33 | const problems = errors && !!Object.keys(errors).length && ( 34 | {mapErrors} 35 | ); 36 | return problems ||
; 37 | }; 38 | 39 | export default Problems; 40 | -------------------------------------------------------------------------------- /packages/client/src/components/cards/Result.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, ReactNode } from 'react'; 2 | import styled, { css } from 'styled-components'; 3 | import bgs from '../../styles/bgs'; 4 | import shapes from '../../styles/shapes'; 5 | import shadows from '../../styles/shadows'; 6 | import layouts from '../../styles/layouts'; 7 | import words from '../../styles/words'; 8 | import states from '../../styles/states'; 9 | import Circles from '../buttons/Circles'; 10 | import MiniButton from '../buttons/MiniButton'; 11 | import PopupMenu from '../menus/PopupMenu'; 12 | 13 | const Wrap = styled('div')` 14 | ${bgs.dark} 15 | ${shapes.simple} 16 | ${shapes.thin} 17 | ${shadows.simple} 18 | ${layouts.space} 19 | ${layouts.rowsCenter} 20 | ${({ onClick }) => 21 | onClick && 22 | css` 23 | cursor: pointer; 24 | ${states.hovered(bgs.darkLight)} 25 | `} 26 | ${({ active }) => 27 | active && 28 | css` 29 | &, 30 | &:hover, 31 | &:active { 32 | ${bgs.darkLighter} 33 | } 34 | `} 35 | `; 36 | 37 | const Name = styled('div')` 38 | margin-right: auto; 39 | `; 40 | 41 | const Note = styled('div')` 42 | ${words.secondary} 43 | text-align: right; 44 | `; 45 | 46 | export interface IResultProps { 47 | children: ReactNode; 48 | note: ReactNode; 49 | menu?: ReactNode; 50 | [name: string]: any; 51 | } 52 | 53 | const Result: FunctionComponent = ({ 54 | children, 55 | note, 56 | menu, 57 | ...args 58 | }) => { 59 | const displayMenu = menu && ( 60 | 61 | 62 | 63 | 64 | 65 | ); 66 | return ( 67 | 68 | {children} 69 | {note} 70 | {displayMenu} 71 | 72 | ); 73 | }; 74 | 75 | export default Result; 76 | -------------------------------------------------------------------------------- /packages/client/src/components/editors/RegularEditor.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react'; 2 | import styled from 'styled-components'; 3 | import bgs from '../../styles/bgs'; 4 | import shapes from '../../styles/shapes'; 5 | import shadows from '../../styles/shadows'; 6 | import Editor, { IEditorProps } from './Editor'; 7 | import layouts from '../../styles/layouts'; 8 | 9 | const Wrap = styled('div')` 10 | ${bgs.dark} 11 | ${shapes.simple} 12 | ${shadows.simple} 13 | ${layouts.columns} 14 | flex-grow: 1; 15 | padding-top: 25px; 16 | `; 17 | 18 | export interface IRegularEditorProps {} 19 | 20 | const RegularEditor: FunctionComponent = ({ 21 | ...args 22 | }) => ( 23 | 24 | 25 | 26 | ); 27 | 28 | export default RegularEditor; 29 | -------------------------------------------------------------------------------- /packages/client/src/components/editors/StatusEditor.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react'; 2 | import styled, { css } from 'styled-components'; 3 | import bgs from '../../styles/bgs'; 4 | import shapes from '../../styles/shapes'; 5 | import shadows from '../../styles/shadows'; 6 | import Editor, { IEditorProps } from './Editor'; 7 | import layouts from '../../styles/layouts'; 8 | import words from '../../styles/words'; 9 | 10 | const Wrap = styled('div')` 11 | ${bgs.dark} 12 | ${shapes.simple} 13 | ${shadows.simple} 14 | ${layouts.columns} 15 | flex-grow: 1; 16 | `; 17 | 18 | const Status = styled('div')` 19 | ${shapes.narrow} 20 | ${shadows.simple} 21 | ${layouts.rowsCenter} 22 | ${layouts.space} 23 | ${bgs.darkLight} 24 | ${words.secondary} 25 | ${({ active }: any) => active && [bgs.marine, words.primary]} 26 | transition: .2s; 27 | margin-bottom: 20px; 28 | `; 29 | 30 | const Title = styled('span')``; 31 | 32 | const Subtitle = styled('span')` 33 | margin-left: auto; 34 | `; 35 | 36 | export interface IStatusEditorProps { 37 | [name: string]: any; 38 | } 39 | 40 | const StatusEditor: FunctionComponent = ({ 41 | ...args 42 | }) => { 43 | const help = args.snippeting 44 | ? 'Press "Enter" to Copy' 45 | : 'Press "Enter" to Select'; 46 | return ( 47 | 48 | 49 | {args.snippeting ? 'Inserting...' : 'Preview'} 50 | {help} 51 | 52 | 53 | 54 | ); 55 | }; 56 | 57 | export default StatusEditor; 58 | -------------------------------------------------------------------------------- /packages/client/src/components/forms/BundleForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react'; 2 | import { Formik, FormikProps, Field } from 'formik'; 3 | import * as Yup from 'yup'; 4 | import GoodButton from '../buttons/GoodButton'; 5 | import Control from '../inputs/Control'; 6 | import LargeInput from '../inputs/LargeInput'; 7 | import { IComponentProps } from '../../utils/components'; 8 | import FormList from '../layouts/FormList'; 9 | import { cleanFormPrefill } from '../../utils/form'; 10 | 11 | export interface IBundleFragment { 12 | name?: string; 13 | readme?: string; 14 | } 15 | 16 | export interface IBundleFormProps extends IComponentProps { 17 | handlers: { 18 | submit: (data: any) => void; 19 | }; 20 | data: { 21 | loading: boolean; 22 | prefill: IBundleFragment; 23 | }; 24 | } 25 | 26 | const BundleForm: FunctionComponent = ({ 27 | data, 28 | handlers, 29 | }) => { 30 | const prefill: IBundleFragment = cleanFormPrefill( 31 | { 32 | name: '', 33 | readme: '', 34 | }, 35 | data.prefill 36 | ); 37 | const validation = Yup.object().shape({ 38 | name: Yup.string() 39 | .trim() 40 | .required(), 41 | readme: Yup.string() 42 | .trim() 43 | .required(), 44 | }); 45 | const form = ({ errors, touched }: FormikProps) => ( 46 | 47 | 55 | 64 | 65 | Create 66 | 67 | 68 | ); 69 | return ( 70 | 76 | ); 77 | }; 78 | 79 | export default BundleForm; 80 | -------------------------------------------------------------------------------- /packages/client/src/components/forms/CardForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react'; 2 | import { Formik, Field, FormikProps } from 'formik'; 3 | import { injectStripe, CardElement } from 'react-stripe-elements'; 4 | import * as Yup from 'yup'; 5 | import GoodButton from '../buttons/GoodButton'; 6 | import Control from '../inputs/Control'; 7 | import { IComponentProps } from '../../utils/components'; 8 | import FormList from '../layouts/FormList'; 9 | import colors from '../../styles/colors'; 10 | import CardInput from '../inputs/CardInput'; 11 | import { cleanFormPrefill } from '../../utils/form'; 12 | 13 | export interface ICardFragment { 14 | name?: string; 15 | } 16 | 17 | export interface ICardFormProps extends IComponentProps { 18 | handlers: { 19 | submit: (data: any) => void; 20 | }; 21 | data: { 22 | prefill: ICardFragment; 23 | loading: boolean; 24 | }; 25 | stripe?: any; 26 | } 27 | 28 | const CardForm: FunctionComponent = ({ data, handlers }) => { 29 | const prefill: ICardFragment = cleanFormPrefill( 30 | { 31 | name: '', 32 | }, 33 | data.prefill 34 | ); 35 | const validation = Yup.object().shape({ 36 | name: Yup.string() 37 | .trim() 38 | .required(), 39 | coupon: Yup.string().trim(), 40 | }); 41 | const form = ({ errors, touched }: FormikProps) => ( 42 | 43 | 51 | 57 | 58 | Save 59 | 60 | 61 | ); 62 | return ( 63 | 69 | ); 70 | }; 71 | 72 | export default injectStripe(CardForm); 73 | -------------------------------------------------------------------------------- /packages/client/src/components/forms/CodeForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react'; 2 | import { Formik, FormikProps, Field } from 'formik'; 3 | import * as Yup from 'yup'; 4 | import Split from '../layouts/Split'; 5 | import List from '../layouts/List'; 6 | import Button from '../buttons/Button'; 7 | import GoodButton from '../buttons/GoodButton'; 8 | import Title from '../texts/Title'; 9 | import Control from '../inputs/Control'; 10 | import RegularEditor from '../editors/RegularEditor'; 11 | import { IComponentProps } from '../../utils/components'; 12 | import { Link } from 'lumbridge'; 13 | import FormList from '../layouts/FormList'; 14 | import { cleanFormPrefill } from '../../utils/form'; 15 | 16 | interface ICodeFragment { 17 | id?: string; 18 | name?: string; 19 | shortcut?: string; 20 | contents?: string; 21 | } 22 | 23 | interface ICodeFormProps extends IComponentProps { 24 | handlers: { 25 | submit: (data: any) => void; 26 | }; 27 | data: { 28 | title: string; 29 | loading: boolean; 30 | prefill: ICodeFragment; 31 | }; 32 | } 33 | 34 | const CodeForm: FunctionComponent = ({ data, handlers }) => { 35 | const prefill: ICodeFragment = cleanFormPrefill( 36 | { 37 | name: '', 38 | shortcut: '', 39 | contents: '', 40 | }, 41 | data.prefill 42 | ); 43 | const validation = Yup.object().shape({ 44 | name: Yup.string() 45 | .trim() 46 | .required(), 47 | shortcut: Yup.string() 48 | .lowercase() 49 | .trim() 50 | .required(), 51 | contents: Yup.string() 52 | .trim() 53 | .required(), 54 | }); 55 | const form = ({ 56 | setFieldValue, 57 | handleChange, 58 | errors, 59 | touched, 60 | }: FormikProps) => { 61 | const editorChange = ({ value }: { value: string }) => 62 | setFieldValue('contents', value); 63 | const nameChange = (event: any) => { 64 | handleChange(event); 65 | if (!prefill.shortcut && !touched.shortcut) { 66 | const short = String(event.target.value || '') 67 | .split(' ') 68 | .filter(word => word && word.length) 69 | .map(word => word.charAt(0)) 70 | .join('') 71 | .toLowerCase(); 72 | setFieldValue('shortcut', short); 73 | } 74 | }; 75 | return ( 76 | 77 | 78 | 79 | 82 |
83 | {data.title} 84 |
85 | 94 | 102 | 103 | Save 104 | 105 |
106 | 110 |
111 |
112 | ); 113 | }; 114 | return ( 115 | 121 | ); 122 | }; 123 | 124 | export default CodeForm; 125 | -------------------------------------------------------------------------------- /packages/client/src/components/forms/LoginForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react'; 2 | import { Formik, FormikProps, Field, Form } from 'formik'; 3 | import * as Yup from 'yup'; 4 | import List from '../layouts/List'; 5 | import GoodButton from '../buttons/GoodButton'; 6 | import { IComponentProps } from '../../utils/components'; 7 | import SimpleInput from '../inputs/SimpleInput'; 8 | import Arrow from '../buttons/Arrow'; 9 | import Onboard from '../layouts/Onboard'; 10 | import { cleanFormPrefill } from '../../utils/form'; 11 | import Problems from '../cards/Problems'; 12 | 13 | interface IUserFragment { 14 | username?: string; 15 | password?: string; 16 | } 17 | 18 | interface ILoginFormProps extends IComponentProps { 19 | handlers: { 20 | submit: (data: any) => void; 21 | }; 22 | data: { 23 | loading: boolean; 24 | }; 25 | } 26 | 27 | const LoginForm: FunctionComponent = ({ data, handlers }) => { 28 | const prefill: IUserFragment = cleanFormPrefill({ 29 | username: '', 30 | password: '', 31 | }); 32 | const validation = Yup.object().shape({ 33 | username: Yup.string() 34 | .trim() 35 | .required(), 36 | password: Yup.string() 37 | .trim() 38 | .min(5) 39 | .required(), 40 | }); 41 | const form = ({ errors, submitCount }: FormikProps) => { 42 | const showProblems = !!submitCount && !!Object.keys(errors).length && ( 43 | 44 | ); 45 | return ( 46 | 47 |
48 | 49 | 55 | 61 | {showProblems} 62 | } 67 | > 68 | Login 69 | 70 | 71 |
72 |
73 | ); 74 | }; 75 | return ( 76 | 82 | ); 83 | }; 84 | 85 | export default LoginForm; 86 | -------------------------------------------------------------------------------- /packages/client/src/components/forms/PasswordForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react'; 2 | import { Formik, FormikProps, Field } from 'formik'; 3 | import * as Yup from 'yup'; 4 | import GoodButton from '../buttons/GoodButton'; 5 | import Control from '../inputs/Control'; 6 | import { IComponentProps } from '../../utils/components'; 7 | import FormList from '../layouts/FormList'; 8 | import { cleanFormPrefill } from '../../utils/form'; 9 | 10 | export interface ISecurityFragment { 11 | oldPassword?: string; 12 | newPassword?: string; 13 | } 14 | 15 | export interface ISecurityFormProps extends IComponentProps { 16 | handlers: { 17 | submit: (data: any) => void; 18 | }; 19 | data: { 20 | loading: boolean; 21 | prefill: ISecurityFragment; 22 | }; 23 | } 24 | 25 | const SecurityForm: FunctionComponent = ({ 26 | data, 27 | handlers, 28 | }) => { 29 | const prefill: ISecurityFragment = cleanFormPrefill( 30 | { 31 | oldPassword: '', 32 | newPassword: '', 33 | }, 34 | data.prefill 35 | ); 36 | const validation = Yup.object().shape({ 37 | oldPassword: Yup.string() 38 | .min(5) 39 | .trim(), 40 | newPassword: Yup.string() 41 | .min(5) 42 | .trim(), 43 | }); 44 | const form = ({ errors, touched }: FormikProps) => ( 45 | 46 | 55 | 64 | 65 | Save 66 | 67 | 68 | ); 69 | return ( 70 | 76 | ); 77 | }; 78 | 79 | export default SecurityForm; 80 | -------------------------------------------------------------------------------- /packages/client/src/components/forms/PreferencesForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react'; 2 | import { Formik, FormikProps, Field } from 'formik'; 3 | import * as Yup from 'yup'; 4 | import GoodButton from '../buttons/GoodButton'; 5 | import Control from '../inputs/Control'; 6 | import { IComponentProps } from '../../utils/components'; 7 | import FormList from '../layouts/FormList'; 8 | import { cleanFormPrefill } from '../../utils/form'; 9 | 10 | export interface IPreferencesFragment { 11 | shortcutOpen?: string; 12 | } 13 | 14 | export interface IPreferencesFormProps extends IComponentProps { 15 | handlers: { 16 | submit: (data: any) => void; 17 | }; 18 | data: { 19 | loading: boolean; 20 | prefill: IPreferencesFragment; 21 | }; 22 | } 23 | 24 | const PreferencesForm: FunctionComponent = ({ 25 | data, 26 | handlers, 27 | }) => { 28 | const prefill: IPreferencesFragment = cleanFormPrefill( 29 | { shortcutOpen: '' }, 30 | data.prefill 31 | ); 32 | const validation = Yup.object().shape({ 33 | shortcutOpen: Yup.string().trim(), 34 | }); 35 | const form = ({ errors, touched }: FormikProps) => ( 36 | 37 | 45 | 46 | Save 47 | 48 | 49 | ); 50 | return ( 51 | 57 | ); 58 | }; 59 | 60 | export default PreferencesForm; 61 | -------------------------------------------------------------------------------- /packages/client/src/components/forms/Problem.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react'; 2 | import styled from 'styled-components'; 3 | import { Formik, Field } from 'formik'; 4 | import * as Yup from 'yup'; 5 | import Background from '../layouts/Background'; 6 | import Control from '../inputs/Control'; 7 | import { IComponentProps } from '../../utils/components'; 8 | import BadButton from '../buttons/BadButton'; 9 | import LargeInput from '../inputs/LargeInput'; 10 | import List from '../layouts/List'; 11 | import Card from '../cards/Card'; 12 | import layouts from '../../styles/layouts'; 13 | import shapes from '../../styles/shapes'; 14 | import Title from '../texts/Title'; 15 | import Subtitle from '../texts/Subtitle'; 16 | import FormList from '../layouts/FormList'; 17 | import { cleanFormPrefill } from '../../utils/form'; 18 | 19 | const Wrap = styled('div')` 20 | ${layouts.center} 21 | ${shapes.padded} 22 | flex-grow: 1; 23 | & > * { 24 | max-width: 400px; 25 | } 26 | `; 27 | 28 | export interface IReport { 29 | message?: string; 30 | } 31 | 32 | export interface IProblemProps extends IComponentProps { 33 | handlers: { 34 | submit: (data: any) => void; 35 | }; 36 | data: { 37 | loading: boolean; 38 | }; 39 | } 40 | 41 | const Problem: FunctionComponent = ({ handlers, data }) => { 42 | const prefill: IReport = cleanFormPrefill({ 43 | message: '', 44 | }); 45 | const validation = Yup.object().shape({ 46 | message: Yup.string().trim(), 47 | }); 48 | const form = () => { 49 | return ( 50 | 51 | 52 | 53 | 54 | 55 |
56 | Ooooooops... 57 | We found an error in our code. 58 |
59 |
60 | 68 | 69 | Relaunch App 70 | 71 |
72 |
73 |
74 |
75 |
76 | ); 77 | }; 78 | return ( 79 | 85 | ); 86 | }; 87 | 88 | export default Problem; 89 | -------------------------------------------------------------------------------- /packages/client/src/components/forms/ProfileForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react'; 2 | import { Formik, FormikProps, Field } from 'formik'; 3 | import * as Yup from 'yup'; 4 | import GoodButton from '../buttons/GoodButton'; 5 | import Control from '../inputs/Control'; 6 | import { IComponentProps } from '../../utils/components'; 7 | import FormList from '../layouts/FormList'; 8 | import { cleanFormPrefill } from '../../utils/form'; 9 | 10 | export interface IProfileFragment { 11 | name?: string; 12 | email?: string; 13 | } 14 | 15 | export interface IProfileFormProps extends IComponentProps { 16 | handlers: { 17 | submit: (data: any) => void; 18 | }; 19 | data: { 20 | loading: boolean; 21 | prefill: IProfileFragment; 22 | }; 23 | } 24 | 25 | const ProfileForm: FunctionComponent = ({ 26 | data, 27 | handlers, 28 | }) => { 29 | const prefill: IProfileFragment = cleanFormPrefill( 30 | { 31 | name: '', 32 | email: '', 33 | }, 34 | data.prefill 35 | ); 36 | const validation = Yup.object().shape({ 37 | name: Yup.string().trim(), 38 | email: Yup.string().trim(), 39 | }); 40 | const form = ({ errors, touched }: FormikProps) => ( 41 | 42 | 50 | 58 | 59 | Save 60 | 61 | 62 | ); 63 | return ( 64 | 70 | ); 71 | }; 72 | 73 | export default ProfileForm; 74 | -------------------------------------------------------------------------------- /packages/client/src/components/forms/SignUpForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react'; 2 | import { Formik, FormikProps, Field, Form } from 'formik'; 3 | import { paramCase } from 'change-case'; 4 | import * as Yup from 'yup'; 5 | import List from '../layouts/List'; 6 | import GoodButton from '../buttons/GoodButton'; 7 | import { IComponentProps } from '../../utils/components'; 8 | import SimpleInput from '../inputs/SimpleInput'; 9 | import Arrow from '../buttons/Arrow'; 10 | import Onboard from '../layouts/Onboard'; 11 | import Problems from '../cards/Problems'; 12 | import { cleanFormPrefill } from '../../utils/form'; 13 | 14 | interface IUserFragment { 15 | username?: string; 16 | password?: string; 17 | email?: string; 18 | } 19 | 20 | interface ISignUpFormProps extends IComponentProps { 21 | handlers: { 22 | submit: (data: any) => void; 23 | }; 24 | data: { 25 | loading: boolean; 26 | }; 27 | } 28 | 29 | const SignUpForm: FunctionComponent = ({ 30 | data, 31 | handlers, 32 | }) => { 33 | const prefill: IUserFragment = cleanFormPrefill({ 34 | username: '', 35 | password: '', 36 | email: '', 37 | }); 38 | const validation = Yup.object().shape({ 39 | username: Yup.string() 40 | .trim() 41 | .required(), 42 | password: Yup.string() 43 | .trim() 44 | .min(5) 45 | .required(), 46 | email: Yup.string() 47 | .trim() 48 | .email() 49 | .required(), 50 | }); 51 | const form = ({ 52 | errors, 53 | submitCount, 54 | setFieldValue, 55 | }: FormikProps) => { 56 | const usernameChange = (event: any) => { 57 | const name = String(event.target.value || ''); 58 | setFieldValue('username', paramCase(name).toLowerCase()); 59 | }; 60 | const showProblems = !!submitCount && !!Object.keys(errors).length && ( 61 | 62 | ); 63 | return ( 64 | 65 |
66 | 67 | 74 | 80 | 86 | {showProblems} 87 | } 92 | > 93 | Sign up 94 | 95 | 96 |
97 |
98 | ); 99 | }; 100 | return ( 101 | 107 | ); 108 | }; 109 | 110 | export default SignUpForm; 111 | -------------------------------------------------------------------------------- /packages/client/src/components/inputs/CardInput.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import bgs from '../../styles/bgs'; 3 | import shapes from '../../styles/shapes'; 4 | import shadows from '../../styles/shadows'; 5 | import states from '../../styles/states'; 6 | import layouts from '../../styles/layouts'; 7 | import { CardElement } from 'react-stripe-elements'; 8 | import colors from '../../styles/colors'; 9 | 10 | export default styled(CardElement).attrs({ 11 | style: { 12 | base: { color: colors.white }, 13 | }, 14 | })` 15 | ${bgs.dark} 16 | ${shapes.narrow} 17 | ${shapes.fill} 18 | ${shadows.simple} 19 | ${layouts.noshrink} 20 | ${states.focused([bgs.darkLight, shadows.pop])} 21 | `; 22 | -------------------------------------------------------------------------------- /packages/client/src/components/inputs/Control.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react'; 2 | import styled, { css } from 'styled-components'; 3 | import layouts from '../../styles/layouts'; 4 | import words from '../../styles/words'; 5 | import colors from '../../styles/colors'; 6 | import SimpleInput from './SimpleInput'; 7 | import animate from '../../styles/animate'; 8 | 9 | const Wrap = styled('div')` 10 | ${layouts.columns} 11 | ${layouts.noshrink} 12 | `; 13 | 14 | const Label = styled('label')` 15 | ${words.normal} 16 | `; 17 | 18 | const Status = styled('div')` 19 | ${words.small} 20 | ${words.secondary} 21 | ${({ problem }: { problem?: boolean | string; [name: string]: any }) => 22 | problem && words.danger} 23 | margin-top: 0.2em; 24 | `; 25 | 26 | const Alert = styled('div')` 27 | ${layouts.rows} 28 | margin-bottom: 0.5em; 29 | `; 30 | 31 | const Sidepop = styled('div')` 32 | ${({ problem }: { problem?: boolean | string; [name: string]: any }) => 33 | problem && 34 | css` 35 | width: 5px; 36 | border-radius: 10px; 37 | background-color: ${colors.dangerLighter}; 38 | margin-right: 5px; 39 | margin-bottom: 2px; 40 | margin-top: 2px; 41 | animation: ${animate.fadeIn} 0.2s linear; 42 | `} 43 | `; 44 | 45 | export interface IControlProps { 46 | label: string; 47 | help: string; 48 | error?: string; 49 | component?: any; 50 | input?: any; 51 | field?: any; 52 | [name: string]: any; 53 | } 54 | 55 | const Control: FunctionComponent = ({ 56 | label, 57 | help, 58 | problem, 59 | component, 60 | input = SimpleInput, 61 | field, 62 | ...args 63 | }) => { 64 | const InputComponent = component || input; 65 | const message = problem || help; 66 | return ( 67 | 68 | 69 | 70 |
71 | 72 | {message} 73 |
74 |
75 | 76 |
77 | ); 78 | }; 79 | 80 | export default Control; 81 | -------------------------------------------------------------------------------- /packages/client/src/components/inputs/LargeInput.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import bgs from '../../styles/bgs'; 3 | import shapes from '../../styles/shapes'; 4 | import shadows from '../../styles/shadows'; 5 | import states from '../../styles/states'; 6 | import words from '../../styles/words'; 7 | import layouts from '../../styles/layouts'; 8 | 9 | export default styled('textarea').attrs({ rows: 3 })` 10 | ${bgs.dark} 11 | ${shapes.narrow} 12 | ${shapes.fill} 13 | ${shadows.simple} 14 | ${layouts.noshrink} 15 | ${states.focused([bgs.darkLight, shadows.pop])} 16 | ${words.multiline} 17 | resize: none; 18 | `; 19 | -------------------------------------------------------------------------------- /packages/client/src/components/inputs/SimpleInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react'; 2 | import styled from 'styled-components'; 3 | import bgs from '../../styles/bgs'; 4 | import shapes from '../../styles/shapes'; 5 | import shadows from '../../styles/shadows'; 6 | import states from '../../styles/states'; 7 | import layouts from '../../styles/layouts'; 8 | 9 | const Wrap = styled('input')` 10 | ${bgs.dark} 11 | ${shapes.narrow} 12 | ${shapes.fill} 13 | ${shadows.simple} 14 | ${layouts.noshrink} 15 | ${states.focused([bgs.darkLight, shadows.pop])} 16 | `; 17 | 18 | export interface IInputProps { 19 | field?: any; 20 | inner?: any; 21 | [name: string]: any; 22 | } 23 | 24 | const SimpleInput: FunctionComponent = ({ 25 | field = {}, 26 | inner, 27 | ...args 28 | }) => ; 29 | 30 | export default SimpleInput; 31 | -------------------------------------------------------------------------------- /packages/client/src/components/layouts/Background.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import bgs from '../../styles/bgs'; 3 | import layouts from '../../styles/layouts'; 4 | 5 | export default styled('div')` 6 | ${bgs.fade} 7 | ${layouts.columns} 8 | flex-grow: 1; 9 | `; 10 | -------------------------------------------------------------------------------- /packages/client/src/components/layouts/Cape.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode, FunctionComponent } from 'react'; 2 | import styled from 'styled-components'; 3 | import layouts from '../../styles/layouts'; 4 | import animate from '../../styles/animate'; 5 | import colors from '../../styles/colors'; 6 | import shapes from '../../styles/shapes'; 7 | 8 | const Wrap = styled('div')` 9 | ${layouts.rows} 10 | ${layouts.spider} 11 | z-index: 10; 12 | background-color: rgba(0, 0, 0, 0.25); 13 | `; 14 | 15 | const Sidebar = styled('div')` 16 | ${shapes.padded} 17 | ${layouts.columns} 18 | align-items: flex-end; 19 | width: 35%; 20 | min-width: 35%; 21 | background-color: ${colors.nightLighter}; 22 | animation: ${animate.slideRight} 0.2s linear; 23 | padding-top: 100px; 24 | padding-left: 30px; 25 | padding-right: 30px; 26 | & > * { 27 | width: 100%; 28 | max-width: 240px; 29 | } 30 | `; 31 | 32 | const Main = styled('div')` 33 | ${shapes.padded} 34 | flex-grow: 1; 35 | background-color: ${colors.nightLight}; 36 | border-left: 1px solid ${colors.night}; 37 | animation: ${animate.slideLeft} 0.2s linear; 38 | padding-top: 100px; 39 | padding-left: 30px; 40 | padding-right: 30px; 41 | & > * { 42 | max-width: 500px; 43 | } 44 | `; 45 | 46 | export interface ICapeProps { 47 | children: ReactNode[]; 48 | } 49 | 50 | const Cape: FunctionComponent = ({ children }) => ( 51 | 52 | {children[0]} 53 |
{children[1]}
54 |
55 | ); 56 | 57 | export default Cape; 58 | -------------------------------------------------------------------------------- /packages/client/src/components/layouts/FormList.tsx: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components'; 2 | import layouts from '../../styles/layouts'; 3 | import { Form } from 'formik'; 4 | 5 | export default styled(Form)` 6 | ${layouts.columns} 7 | flex-grow: 1; 8 | & > * { 9 | margin-bottom: 15px; 10 | &:last-child { 11 | margin-bottom: 0; 12 | } 13 | } 14 | `; 15 | -------------------------------------------------------------------------------- /packages/client/src/components/layouts/List.tsx: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components'; 2 | import layouts from '../../styles/layouts'; 3 | 4 | export default styled('div')` 5 | ${layouts.columns} 6 | flex-grow: 1; 7 | & > * { 8 | margin-bottom: ${({ 9 | wide, 10 | }: { 11 | wide?: string | boolean; 12 | [name: string]: any; 13 | }) => (wide ? '15px' : '10px')}; 14 | &:last-child { 15 | margin-bottom: 0; 16 | } 17 | } 18 | `; 19 | -------------------------------------------------------------------------------- /packages/client/src/components/layouts/Marketplace.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react'; 2 | import styled from 'styled-components'; 3 | import { IComponentProps } from '../../utils/components'; 4 | import Subtitle from '../texts/Subtitle'; 5 | import bgs from '../../styles/bgs'; 6 | import shapes from '../../styles/shapes'; 7 | import shadows from '../../styles/shadows'; 8 | import layouts from '../../styles/layouts'; 9 | import words from '../../styles/words'; 10 | import states from '../../styles/states'; 11 | import { IToggle } from '../statefuls/Toggle'; 12 | import PreviewBundleModal from '../modals/PreviewBundleModal'; 13 | 14 | const Wrap = styled('div')` 15 | ${layouts.rows} 16 | ${shapes.padded} 17 | flex-grow: 1; 18 | flex-wrap: wrap; 19 | padding-right: 0; 20 | align-content: flex-start; 21 | `; 22 | 23 | const Item = styled('div')` 24 | ${bgs.dark} 25 | ${shapes.simple} 26 | ${shadows.simple} 27 | ${layouts.space} 28 | ${states.hovered(bgs.darkLight)} 29 | ${states.clicked([bgs.dark, shadows.none])} 30 | width: 25.1%; 31 | margin-right: 15px; 32 | margin-bottom: 15px; 33 | flex-grow: 1; 34 | `; 35 | 36 | const Name = styled('div')` 37 | ${words.normal} 38 | margin-bottom: 4px; 39 | `; 40 | 41 | const Readme = styled('div')` 42 | ${words.secondary} 43 | ${words.small} 44 | `; 45 | 46 | export interface IBundleFragment { 47 | id: string; 48 | name: string; 49 | readme: string; 50 | codeCount: number; 51 | } 52 | 53 | export interface IMarketplaceProps extends IComponentProps { 54 | data: { 55 | bundles: IBundleFragment[]; 56 | }; 57 | handlers: { 58 | subscribe: (bundle: IBundleFragment) => any; 59 | }; 60 | } 61 | 62 | const Marketplace: FunctionComponent = ({ 63 | data, 64 | handlers, 65 | }) => { 66 | const bundles = data.bundles.map((bundle: IBundleFragment) => { 67 | const { id, name, readme } = bundle; 68 | const item = ({ open }: IToggle) => ( 69 | 70 | {name} 71 | {readme} 72 | 73 | ); 74 | const modalData = { bundle }; 75 | return ( 76 | 77 | {item} 78 | 79 | ); 80 | }); 81 | return {bundles}; 82 | }; 83 | 84 | export default Marketplace; 85 | -------------------------------------------------------------------------------- /packages/client/src/components/layouts/Modal.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode, FunctionComponent, useEffect } from 'react'; 2 | import OutsideClickHandler from 'react-outside-click-handler'; 3 | import styled from 'styled-components'; 4 | import layouts from '../../styles/layouts'; 5 | import shapes from '../../styles/shapes'; 6 | import Toggle, { IToggle } from '../statefuls/Toggle'; 7 | import animate from '../../styles/animate'; 8 | import words from '../../styles/words'; 9 | import Portal from '../statefuls/Portal'; 10 | 11 | const Wrap = styled('div')` 12 | ${layouts.center} 13 | ${shapes.padded} 14 | ${layouts.spider} 15 | z-index: 10; 16 | background-color: rgba(100, 100, 100, 0.2); 17 | animation: ${animate.fadeIn} 0.2s linear; 18 | `; 19 | 20 | const Center = styled('div')` 21 | ${words.normal} 22 | ${words.primary} 23 | `; 24 | 25 | const Close = styled('div')` 26 | ${words.small} 27 | ${layouts.rowsCenter} 28 | ${shapes.padded} 29 | padding-bottom: 0; 30 | margin-bottom: -5px; 31 | justify-content: flex-end; 32 | cursor: pointer; 33 | `; 34 | 35 | export interface IModalProps { 36 | component: (bag: IToggle) => ReactNode; 37 | children: (bag: IToggle) => ReactNode; 38 | } 39 | 40 | const Modal: FunctionComponent = ({ component, children }) => { 41 | let previousActiveItem: Element | null; 42 | useEffect(() => { 43 | const execute = () => { 44 | previousActiveItem = document.activeElement; 45 | }; 46 | window.addEventListener('click', execute); 47 | return () => window.removeEventListener('click', execute); 48 | }, []); 49 | const toggleable = ({ toggle, active, close, ...others }: IToggle) => { 50 | const closeNoFocus = () => { 51 | if ( 52 | !previousActiveItem || 53 | previousActiveItem === document.body || 54 | previousActiveItem.tagName === 'BUTTON' 55 | ) { 56 | close(); 57 | } 58 | }; 59 | const popup = ( 60 | 61 | 62 |
63 | x close. 64 | 65 | {component({ toggle, active, close, ...others })} 66 | 67 |
68 |
69 |
70 | ); 71 | return ( 72 | <> 73 | {children({ toggle, active, close, ...others })} 74 | {active && popup} 75 | 76 | ); 77 | }; 78 | return {toggleable}; 79 | }; 80 | 81 | export default Modal; 82 | -------------------------------------------------------------------------------- /packages/client/src/components/layouts/Onboard.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, ReactNode } from 'react'; 2 | import styled from 'styled-components'; 3 | import { IComponentProps } from '../../utils/components'; 4 | import bgs from '../../styles/bgs'; 5 | import layouts from '../../styles/layouts'; 6 | import Container from '../cards/Container'; 7 | import animate from '../../styles/animate'; 8 | import { Link } from 'lumbridge'; 9 | import colors from '../../styles/colors'; 10 | import { loadAsset } from '../../utils/assets'; 11 | 12 | const Wrap = styled('div')` 13 | ${layouts.center} 14 | ${bgs.fade} 15 | flex-grow: 1; 16 | `; 17 | 18 | const Drag = styled('div')` 19 | position: absolute; 20 | top: 0; 21 | left: 0; 22 | right: 0; 23 | z-index: 100; 24 | height: 40px; 25 | -webkit-app-region: drag; 26 | `; 27 | 28 | const Logo = styled('img')` 29 | height: 35px; 30 | margin: 0 auto 25px; 31 | opacity: 0.6; 32 | `; 33 | 34 | const Fade = styled('div')` 35 | animation: ${animate.fadeIn} 0.5s linear; 36 | `; 37 | 38 | const Back = styled(Link)` 39 | color: ${colors.nightLighter}; 40 | text-align: center; 41 | margin: 20px 0 0; 42 | cursor: pointer; 43 | transition: 0.2s; 44 | &:hover { 45 | color: ${colors.shade}; 46 | } 47 | `; 48 | 49 | export interface IOnboardProps extends IComponentProps { 50 | children: ReactNode; 51 | back?: boolean; 52 | } 53 | 54 | const Onboard: FunctionComponent = ({ children, back }) => { 55 | return ( 56 | 57 | 58 | 59 | 60 | {children} 61 | {back && Go back.} 62 | 63 | 64 | ); 65 | }; 66 | 67 | export default Onboard; 68 | -------------------------------------------------------------------------------- /packages/client/src/components/layouts/Split.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode, FunctionComponent } from 'react'; 2 | import styled, { css } from 'styled-components'; 3 | import layouts from '../../styles/layouts'; 4 | import shapes from '../../styles/shapes'; 5 | 6 | const Wrap = styled('div')` 7 | ${layouts.rows} 8 | ${shapes.padded} 9 | ${({ modal }: { modal?: boolean | string; [name: string]: any }) => 10 | modal && 11 | css` 12 | width: 800px; 13 | max-width: 100%; 14 | max-height: 600px; 15 | `} 16 | flex-grow: 1; 17 | `; 18 | 19 | const Sidebar = styled('div')` 20 | ${layouts.columns} 21 | width: 35%; 22 | min-width: 35%; 23 | margin-right: 15px; 24 | ${({ 25 | reverse, 26 | }: { 27 | reverse?: boolean | string; 28 | middle?: boolean | string; 29 | [name: string]: any; 30 | }) => 31 | reverse && 32 | css` 33 | margin-right: 0; 34 | margin-left: 15px; 35 | order: 1; 36 | `} 37 | ${({ middle }: any) => 38 | middle && 39 | css` 40 | width: 50%; 41 | min-width: 50%; 42 | `} 43 | `; 44 | 45 | const Main = styled('div')` 46 | ${layouts.columns} 47 | flex-grow: 1; 48 | `; 49 | 50 | export interface ISplitProps { 51 | children: ReactNode[]; 52 | reverse?: boolean; 53 | middle?: boolean; 54 | [name: string]: any; 55 | } 56 | 57 | const Split: FunctionComponent = ({ 58 | children, 59 | reverse, 60 | middle, 61 | ...args 62 | }: ISplitProps) => ( 63 | 64 | 65 | {children[0]} 66 | 67 |
{children[1]}
68 |
69 | ); 70 | 71 | export default Split; 72 | -------------------------------------------------------------------------------- /packages/client/src/components/lists/ChooseBundle.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react'; 2 | import styled from 'styled-components'; 3 | import { IComponentProps } from '../../utils/components'; 4 | import Result from '../cards/Result'; 5 | import List from '../layouts/List'; 6 | 7 | const Wrap = styled('div')` 8 | flex-grow: 1; 9 | overflow: auto; 10 | `; 11 | 12 | export interface IBundleFragment { 13 | id: string; 14 | name: string; 15 | codeCount: number; 16 | } 17 | 18 | export interface IChooseBundleProps extends IComponentProps { 19 | data: { 20 | bundles: IBundleFragment[]; 21 | }; 22 | handlers: { 23 | choose: (bundle: IBundleFragment) => any; 24 | }; 25 | } 26 | 27 | const ChooseBundle: FunctionComponent = ({ 28 | data, 29 | handlers, 30 | }) => { 31 | const bundles = data.bundles.map((bundle: IBundleFragment) => { 32 | const { id, name, codeCount } = bundle; 33 | const choose = () => handlers.choose(bundle); 34 | return ( 35 | 40 | {name} 41 | 42 | ); 43 | }); 44 | return ( 45 | 46 | {bundles} 47 | 48 | ); 49 | }; 50 | 51 | export default ChooseBundle; 52 | -------------------------------------------------------------------------------- /packages/client/src/components/lists/ChooseCode.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, useEffect } from 'react'; 2 | import styled from 'styled-components'; 3 | import keycode from 'keycode'; 4 | import { IComponentProps } from '../../utils/components'; 5 | import Result from '../cards/Result'; 6 | import List from '../layouts/List'; 7 | import useKeyboard from '../../effects/useKeyboard'; 8 | import CodeMenu from '../../containers/menus/CodeMenu'; 9 | 10 | const Wrap = styled('div')` 11 | flex-grow: 1; 12 | overflow: auto; 13 | `; 14 | 15 | export interface ICodeFragment { 16 | id: string; 17 | name: string; 18 | shortcut: string; 19 | contents: string; 20 | } 21 | 22 | export interface IChooseCodeProps extends IComponentProps { 23 | data: { 24 | codes: ICodeFragment[]; 25 | focusedCode: ICodeFragment; 26 | editing: boolean; 27 | }; 28 | handlers: { 29 | focusCode: (code: ICodeFragment, force?: boolean) => any; 30 | chooseCode: (code?: ICodeFragment) => any; 31 | clipboardCopyCode: (data: { value: string; id?: string }) => void; 32 | deleteCode: (data: { id: string }) => void; 33 | cloneCode: (data: { id: string }) => void; 34 | }; 35 | } 36 | 37 | const ChooseCode: FunctionComponent = ({ 38 | data, 39 | handlers, 40 | }) => { 41 | const { focusedCode } = data; 42 | const { activeKey } = useKeyboard('keydown'); 43 | useEffect( 44 | () => { 45 | if (activeKey && data.codes.length) { 46 | if (data.editing) { 47 | if (keycode.isEventKey(activeKey, 'escape')) { 48 | handlers.chooseCode(); 49 | } 50 | } else { 51 | const index = focusedCode 52 | ? data.codes.findIndex(code => code.id === focusedCode.id) 53 | : -1; 54 | const lastIndex = data.codes.length - 1; 55 | const prevIndex = index - 1; 56 | const nextIndex = index + 1; 57 | if (keycode.isEventKey(activeKey, 'up')) { 58 | const focus = 59 | index === -1 60 | ? data.codes[0] 61 | : data.codes[index <= 0 ? 0 : prevIndex]; 62 | handlers.focusCode(focus); 63 | } 64 | if (keycode.isEventKey(activeKey, 'down')) { 65 | const focus = 66 | index === -1 67 | ? data.codes[0] 68 | : data.codes[index >= lastIndex ? lastIndex : nextIndex]; 69 | handlers.focusCode(focus); 70 | } 71 | if (keycode.isEventKey(activeKey, 'enter')) { 72 | handlers.chooseCode(focusedCode); 73 | } 74 | } 75 | } 76 | }, 77 | [activeKey] 78 | ); 79 | const codes = data.codes.map((code: ICodeFragment) => { 80 | const { id, name, shortcut } = code; 81 | const focus = () => handlers.focusCode(code, true); 82 | const menu = ; 83 | return ( 84 | 91 | {name} 92 | 93 | ); 94 | }); 95 | return ( 96 | 97 | {codes} 98 | 99 | ); 100 | }; 101 | 102 | export default ChooseCode; 103 | -------------------------------------------------------------------------------- /packages/client/src/components/menus/CardMenu.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, ReactNode } from 'react'; 2 | import styled from 'styled-components'; 3 | import layouts from '../../styles/layouts'; 4 | import bgs from '../../styles/bgs'; 5 | import shadows from '../../styles/shadows'; 6 | import shapes from '../../styles/shapes'; 7 | import states from '../../styles/states'; 8 | import List from '../layouts/List'; 9 | 10 | const Wrap = styled('div')``; 11 | 12 | export interface ICardMenuProps { 13 | children: ReactNode; 14 | } 15 | 16 | const CardMenu: FunctionComponent & { 17 | Item: any; 18 | } = ({ children }) => ( 19 | 20 | {children} 21 | 22 | ); 23 | 24 | CardMenu.Item = styled('div')` 25 | ${shapes.narrow} 26 | ${shadows.simple} 27 | ${layouts.space} 28 | ${bgs.darkLight} 29 | ${states.hovered(bgs.darkLighter)} 30 | ${states.clicked([bgs.darkLight, shadows.none])} 31 | ${({ 32 | ...args 33 | }: { 34 | as?: boolean | string; 35 | to?: boolean | string; 36 | [name: string]: any; 37 | }) => null} 38 | `; 39 | 40 | export default CardMenu; 41 | -------------------------------------------------------------------------------- /packages/client/src/components/menus/ChooseAuth.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react'; 2 | import styled from 'styled-components'; 3 | import colors from '../../styles/colors'; 4 | import Button from '../buttons/Button'; 5 | import List from '../layouts/List'; 6 | import Onboard from '../layouts/Onboard'; 7 | import GoodButton from '../buttons/GoodButton'; 8 | import Arrow from '../buttons/Arrow'; 9 | import { Link } from 'lumbridge'; 10 | import layouts from '../../styles/layouts'; 11 | 12 | const Divider = styled('div')` 13 | ${layouts.rowsCenter} 14 | color: ${colors.nightDarker}; 15 | margin: 0 10px 10px; 16 | `; 17 | 18 | const Line = styled('div')` 19 | background-color: ${colors.night}; 20 | height: 1px; 21 | flex-grow: 1; 22 | `; 23 | 24 | const Or = styled('div')` 25 | margin: 0 10px; 26 | `; 27 | 28 | const LinkWrap = styled(Link)` 29 | text-decoration: none; 30 | `; 31 | 32 | const LoginButton = styled(GoodButton)` 33 | width: 100%; 34 | `; 35 | 36 | const SignUpButton = styled(Button)` 37 | width: 100%; 38 | `; 39 | 40 | export interface IChooseAuthProps {} 41 | 42 | const ChooseAuth: FunctionComponent = () => { 43 | return ( 44 | 45 | 46 | 47 | }> 48 | Login 49 | 50 | 51 | 52 | 53 | or 54 | 55 | 56 | 57 | }>Sign up 58 | 59 | 60 | 61 | ); 62 | }; 63 | 64 | export default ChooseAuth; 65 | -------------------------------------------------------------------------------- /packages/client/src/components/menus/Header.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, ReactNode } from 'react'; 2 | import styled from 'styled-components'; 3 | import { Link } from 'lumbridge'; 4 | import bgs from '../../styles/bgs'; 5 | import colors from '../../styles/colors'; 6 | import shadows from '../../styles/shadows'; 7 | import layouts from '../../styles/layouts'; 8 | import Circles from '../buttons/Circles'; 9 | import PopupMenu from './PopupMenu'; 10 | import MiniButton from '../buttons/MiniButton'; 11 | 12 | const Wrap = styled('div').attrs({ borderless: 'true' })` 13 | ${bgs.dark} 14 | ${shadows.simple} 15 | border-bottom: 1px solid ${colors.nightDark}; 16 | flex-shrink: 0; 17 | `; 18 | 19 | const Forward = styled('div')` 20 | ${layouts.rowsCenter} 21 | justify-content: flex-end; 22 | position: relative; 23 | z-index: 100; 24 | height: 40px; 25 | padding: 0 15px; 26 | -webkit-app-region: drag; 27 | `; 28 | 29 | export interface IHeaderProps { 30 | menu: ReactNode; 31 | } 32 | 33 | const Header: FunctionComponent = ({ menu }) => ( 34 | 35 | 36 | 37 | Dashboard 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | ); 47 | 48 | export default Header; 49 | -------------------------------------------------------------------------------- /packages/client/src/components/menus/PopupMenu.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, ReactNode } from 'react'; 2 | import styled from 'styled-components'; 3 | import OutsideClickHandler from 'react-outside-click-handler'; 4 | import layouts from '../../styles/layouts'; 5 | import bgs from '../../styles/bgs'; 6 | import shadows from '../../styles/shadows'; 7 | import shapes from '../../styles/shapes'; 8 | import Toggle, { IToggle } from '../statefuls/Toggle'; 9 | import states from '../../styles/states'; 10 | import words from '../../styles/words'; 11 | 12 | const Container = styled('div')` 13 | position: relative; 14 | flex-shrink: 0; 15 | `; 16 | 17 | const Wrap = styled('div')` 18 | ${bgs.darkLighter} 19 | ${shadows.simple} 20 | ${shapes.simple} 21 | padding-left: 0; 22 | padding-right: 0; 23 | position: absolute; 24 | right: 5px; 25 | top: 5px; 26 | z-index: 20; 27 | `; 28 | 29 | export interface IPopupMenuProps { 30 | children: ReactNode; 31 | items: ReactNode; 32 | } 33 | 34 | const PopupMenu: FunctionComponent & { 35 | Item: any; 36 | List: any; 37 | } = ({ children, items }) => { 38 | const toggleable = ({ active, open, close, ...args }: IToggle) => { 39 | const popup = ( 40 | 41 | 42 |
{items}
43 |
44 |
45 | ); 46 | const openClose = () => !active && open(); 47 | return ( 48 | 49 | {children} 50 | {active && popup} 51 | 52 | ); 53 | }; 54 | return {toggleable}; 55 | }; 56 | 57 | PopupMenu.Item = styled('div').attrs({ borderless: 'true' })` 58 | ${({ 59 | ...args 60 | }: { 61 | as?: boolean | string; 62 | to?: boolean | string; 63 | [name: string]: any; 64 | }) => null} 65 | ${bgs.darkLight} 66 | ${states.hovered(bgs.dark)} 67 | ${shapes.thin} 68 | ${layouts.rowsCenter} 69 | ${words.small} 70 | width: 200px; 71 | `; 72 | 73 | PopupMenu.List = styled('div')` 74 | ${layouts.columns} 75 | `; 76 | 77 | export default PopupMenu; 78 | -------------------------------------------------------------------------------- /packages/client/src/components/modals/BundleModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, useState } from 'react'; 2 | import { IComponentProps } from '../../utils/components'; 3 | import Split from '../layouts/Split'; 4 | import Card from '../cards/Card'; 5 | import Modal from '../layouts/Modal'; 6 | import { IToggle } from '../statefuls/Toggle'; 7 | import Title from '../texts/Title'; 8 | import Subtitle from '../texts/Subtitle'; 9 | import List from '../layouts/List'; 10 | import SelectBundle from '../../containers/modals/SelectBundle'; 11 | import CreateBundle from '../../containers/modals/CreateBundle'; 12 | import Button from '../buttons/Button'; 13 | 14 | export interface IBundle { 15 | id: string; 16 | name: string; 17 | codeCount: number; 18 | } 19 | 20 | export interface IBundleModalProps extends IComponentProps { 21 | handlers: { 22 | choose: (bundle: IBundle) => any; 23 | }; 24 | } 25 | 26 | const BundleModal: FunctionComponent = ({ 27 | handlers: bundleHandlers, 28 | }) => { 29 | const [currentBundle, setCurrentBundle] = useState(null); 30 | const modal = ({ close }: IToggle) => { 31 | const handlers = { 32 | choose: (bundle: any) => { 33 | setCurrentBundle(bundle); 34 | bundleHandlers.choose(bundle); 35 | close(); 36 | }, 37 | }; 38 | return ( 39 | 40 | 41 | 42 | Select Bundle 43 | Create or select a bundle. 44 | 45 | 46 | 47 | 48 | Create Bundle 49 |
50 | 51 |
52 |
53 | ); 54 | }; 55 | const button = ({ open }: IToggle) => ( 56 | 59 | ); 60 | return {button}; 61 | }; 62 | 63 | export default BundleModal; 64 | -------------------------------------------------------------------------------- /packages/client/src/components/modals/PreviewBundleModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, ReactNode } from 'react'; 2 | import { IComponentProps } from '../../utils/components'; 3 | import Split from '../layouts/Split'; 4 | import Card from '../cards/Card'; 5 | import Modal from '../layouts/Modal'; 6 | import { IToggle } from '../statefuls/Toggle'; 7 | import { IBundleFragment } from '../layouts/Marketplace'; 8 | import List from '../layouts/List'; 9 | import Title from '../texts/Title'; 10 | import Subtitle from '../texts/Subtitle'; 11 | import words from '../../styles/words'; 12 | import styled from 'styled-components'; 13 | import RegularEditor from '../editors/RegularEditor'; 14 | import GoodButton from '../buttons/GoodButton'; 15 | import layouts from '../../styles/layouts'; 16 | import Button from '../buttons/Button'; 17 | import { loadAsset } from '../../utils/assets'; 18 | 19 | const Readme = styled('div')` 20 | ${words.secondary} 21 | `; 22 | 23 | const Top = styled('div')` 24 | ${layouts.rowsCenter} 25 | & > *:nth-child(2) { 26 | margin: 0 10px; 27 | } 28 | `; 29 | 30 | const Left = styled('img')` 31 | transform: rotate(-90deg); 32 | display: block; 33 | height: 18px; 34 | filter: invert(35%); 35 | `; 36 | 37 | const Right = styled('img')` 38 | transform: rotate(90deg); 39 | display: block; 40 | height: 18px; 41 | filter: invert(35%); 42 | `; 43 | 44 | export interface IPreviewBundleModalProps extends IComponentProps { 45 | children: (bag: IToggle) => ReactNode; 46 | data: { 47 | bundle: IBundleFragment; 48 | }; 49 | handlers: { 50 | subscribe: (bundle: IBundleFragment) => any; 51 | }; 52 | } 53 | 54 | const PreviewBundleModal: FunctionComponent = ({ 55 | children, 56 | data, 57 | handlers, 58 | }) => { 59 | const chooseBundle = () => handlers.subscribe(data.bundle); 60 | const modal = () => ( 61 | 62 | 63 | 64 | {data.bundle.name} 65 | {data.bundle.codeCount} Snippets 66 |
67 | 68 | Get this bundle 69 | 70 |
71 | 72 | {data.bundle.readme} 73 | 74 |
75 | 76 | 77 | 80 | 81 | React Component 82 | 83 | 86 | 87 | 91 | 92 |
93 | ); 94 | return {children}; 95 | }; 96 | 97 | export default PreviewBundleModal; 98 | -------------------------------------------------------------------------------- /packages/client/src/components/statefuls/Portal.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, Component } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | export interface IPortalProps { 5 | children: ReactNode; 6 | } 7 | 8 | export interface IPortalState {} 9 | 10 | export default class Portal extends Component { 11 | private root: HTMLElement | null; 12 | private hook: HTMLElement; 13 | 14 | constructor(props: IPortalProps) { 15 | super(props); 16 | this.root = document.getElementById('root'); 17 | this.hook = document.createElement('div'); 18 | } 19 | 20 | public componentDidMount() { 21 | if (this.root) { 22 | this.root.appendChild(this.hook); 23 | } 24 | } 25 | 26 | public componentWillUnmount() { 27 | if (this.root) { 28 | this.root.removeChild(this.hook); 29 | } 30 | } 31 | 32 | public render() { 33 | const { children } = this.props; 34 | return ReactDOM.createPortal(children, this.hook); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/client/src/components/statefuls/Toggle.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, Component } from 'react'; 2 | 3 | export interface IToggle { 4 | active: boolean; 5 | toggle: (override?: boolean) => any; 6 | open: () => any; 7 | close: () => any; 8 | } 9 | 10 | export interface IToggleProps { 11 | children: (props: IToggle) => ReactNode; 12 | } 13 | 14 | export interface IToggleState { 15 | active: boolean; 16 | } 17 | 18 | export default class Toggle extends Component { 19 | constructor(props: IToggleProps) { 20 | super(props); 21 | this.state = { 22 | active: false, 23 | }; 24 | } 25 | 26 | public render() { 27 | const { active } = this.state; 28 | const { children } = this.props; 29 | return children({ 30 | active, 31 | toggle: this.toggle, 32 | open: () => this.toggle(true), 33 | close: () => this.toggle(false), 34 | }); 35 | } 36 | 37 | private toggle = (override?: boolean) => { 38 | const { active } = this.state; 39 | this.setState({ active: override !== undefined ? override : !active }); 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /packages/client/src/components/texts/Subtitle.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import words from '../../styles/words'; 3 | 4 | export default styled('span')` 5 | ${words.secondary} 6 | `; 7 | -------------------------------------------------------------------------------- /packages/client/src/components/texts/Title.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import words from '../../styles/words'; 3 | 4 | export default styled('span')` 5 | ${words.title} 6 | margin-bottom: 5px; 7 | `; 8 | -------------------------------------------------------------------------------- /packages/client/src/components/toast/Container.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | 5 | const Wrap = styled.div` 6 | padding: 20px; 7 | position: fixed; 8 | z-index: 100; 9 | bottom: 0; 10 | right: 0; 11 | `; 12 | 13 | const Toast = ({ children }) => {children}; 14 | 15 | Toast.propTypes = { 16 | children: PropTypes.node.isRequired, 17 | }; 18 | 19 | export default Toast; 20 | -------------------------------------------------------------------------------- /packages/client/src/components/toast/Portal.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import PropTypes from 'prop-types'; 4 | 5 | export default class Portal extends Component { 6 | static propTypes = { 7 | children: PropTypes.node.isRequired, 8 | }; 9 | 10 | constructor(...args) { 11 | super(...args); 12 | this.dochook = document.createElement('div'); 13 | } 14 | 15 | componentDidMount() { 16 | document.body.appendChild(this.dochook); 17 | } 18 | 19 | componentWillUnmount() { 20 | document.body.removeChild(this.dochook); 21 | } 22 | 23 | render() { 24 | const { children } = this.props; 25 | return ReactDOM.createPortal(children, this.dochook); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/client/src/components/toast/Toast.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled, { css, keyframes } from 'styled-components'; 4 | import bgs from '../../styles/bgs'; 5 | import shapes from '../../styles/shapes'; 6 | import words from '../../styles/words'; 7 | import colors from '../../styles/colors'; 8 | 9 | const slideIn = keyframes` 10 | 0% { 11 | opacity: 0; 12 | transform: translateY(150%); 13 | } 14 | 100% { 15 | opacity: 1; 16 | transform: translateY(0%); 17 | } 18 | `; 19 | 20 | const Wrap = styled.div` 21 | ${bgs.darkLighter} 22 | ${shapes.simple} 23 | width: 300px; 24 | margin-top: 10px; 25 | ${({ type }) => 26 | type === 'error' && 27 | css` 28 | border-left: 5px solid ${colors.dangerLighter}; 29 | `}; 30 | opacity: 0; 31 | transform: translateY(150%); 32 | animation: 0.5s forwards ${slideIn}; 33 | `; 34 | 35 | const Top = styled.div` 36 | display: flex; 37 | align-items: flex-start; 38 | margin-bottom: 20px; 39 | `; 40 | 41 | const Label = styled.div` 42 | ${bgs.dark} 43 | ${shapes.simple} 44 | ${shapes.mini} 45 | ${words.small} 46 | `; 47 | 48 | const Close = styled.div` 49 | margin-left: auto; 50 | cursor: pointer; 51 | font-family: monospace; 52 | `; 53 | 54 | const Contents = styled.div` 55 | ${words.multiline} 56 | white-space: pre-wrap; 57 | `; 58 | 59 | const Toast = ({ close, contents, type, ...props }) => { 60 | let label; 61 | switch (type) { 62 | case 'error': 63 | label = 'Error'; 64 | break; 65 | default: 66 | label = 'Alert'; 67 | break; 68 | } 69 | return ( 70 | 71 | 72 | 73 | X 74 | 75 | {contents.replace('GraphQL error: ', '')} 76 | 77 | ); 78 | }; 79 | 80 | Toast.propTypes = { 81 | close: PropTypes.func.isRequired, 82 | contents: PropTypes.string.isRequired, 83 | }; 84 | 85 | export default Toast; 86 | -------------------------------------------------------------------------------- /packages/client/src/components/toast/Toaster.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import ToasterContext from './ToasterContext'; 4 | import Portal from './Portal'; 5 | import Container from './Container'; 6 | import Toast from './Toast'; 7 | 8 | export default class Toaster extends Component { 9 | static propTypes = { 10 | children: PropTypes.node.isRequired, 11 | timeout: PropTypes.number, 12 | toast: PropTypes.func, 13 | toastContainer: PropTypes.func, 14 | }; 15 | 16 | static defaultProps = { 17 | timeout: 4000, 18 | toast: null, 19 | toastContainer: null, 20 | }; 21 | 22 | constructor(...args) { 23 | super(...args); 24 | this.dochook = document.createElement('div'); 25 | this.state = { 26 | toasts: new Map(), 27 | }; 28 | } 29 | 30 | componentDidMount() { 31 | document.body.appendChild(this.dochook); 32 | } 33 | 34 | componentWillUnmount() { 35 | document.body.removeChild(this.dochook); 36 | } 37 | 38 | addToast = toast => { 39 | const id = Math.random() 40 | .toString(36) 41 | .substr(3); 42 | const { toasts } = this.state; 43 | this.setState({ 44 | toasts: toasts.set(id, toast), 45 | }); 46 | const { timeout } = this.props; 47 | if (timeout) { 48 | setTimeout(() => this.removeToast(id), timeout); 49 | } 50 | return id; 51 | }; 52 | 53 | removeToast = id => { 54 | const { toasts } = this.state; 55 | toasts.delete(id); 56 | this.setState({ toasts }); 57 | }; 58 | 59 | render() { 60 | const { toasts } = this.state; 61 | const { children, toast, toastContainer } = this.props; 62 | const ToastComponent = toast || Toast; 63 | const ToastContainerComponent = toastContainer || Container; 64 | return ( 65 | 66 | 72 | {children} 73 | 74 | 75 | 76 | {[...toasts.entries()].map(([id, args]) => ( 77 | this.removeToast(id)} 80 | {...args} 81 | /> 82 | ))} 83 | 84 | 85 | 86 | ); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /packages/client/src/components/toast/ToasterContext.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default React.createContext(); 4 | -------------------------------------------------------------------------------- /packages/client/src/components/toast/withToaster.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ToasterContext from './ToasterContext'; 3 | 4 | export default Component => ({ ...props }) => ( 5 | 6 | {({ ...contextProps }) => } 7 | 8 | ); 9 | -------------------------------------------------------------------------------- /packages/client/src/config.ts: -------------------------------------------------------------------------------- 1 | export interface IGlobalConfigDefaults { 2 | environment: string; 3 | appName: string; 4 | assetPath: string; 5 | website: string; 6 | stripeKey: string; 7 | segmentId: string; 8 | sentryDSN: string; 9 | intercom: string; 10 | } 11 | 12 | const environment = process.env.REACT_APP_NODE_ENV || 'development'; 13 | 14 | const defaults: IGlobalConfigDefaults = { 15 | environment, 16 | appName: 'Forge', 17 | assetPath: './assets', 18 | website: 'https://useforge.co', 19 | stripeKey: process.env.REACT_APP_STRIPE_KEY as string, 20 | segmentId: process.env.REACT_APP_SEGMENT_ID as string, 21 | sentryDSN: process.env.REACT_APP_SENTRY_DSN as string, 22 | intercom: process.env.REACT_APP_INTERCOM_ID as string, 23 | }; 24 | 25 | export interface IGlobalConfig extends IGlobalConfigDefaults { 26 | debug: boolean; 27 | urls: { 28 | spa: string; 29 | api: string; 30 | }; 31 | } 32 | 33 | let config: IGlobalConfig; 34 | 35 | switch (environment) { 36 | case 'production': 37 | config = { 38 | ...defaults, 39 | debug: false, 40 | urls: { 41 | spa: 'https://app.useforge.co', 42 | api: 'https://api.v1.useforge.co', 43 | }, 44 | }; 45 | break; 46 | default: 47 | config = { 48 | ...defaults, 49 | debug: true, 50 | urls: { 51 | spa: 'http://localhost:3000', 52 | api: 'http://localhost:4000', 53 | }, 54 | }; 55 | break; 56 | } 57 | 58 | export default config; 59 | -------------------------------------------------------------------------------- /packages/client/src/containers/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, useEffect, useState } from 'react'; 2 | import gql from 'graphql-tag'; 3 | import AuthRoutes from './routes/AuthRoutes'; 4 | import authStore from '../utils/authStore'; 5 | import authScope, { 6 | retrieveLocalAuth, 7 | saveLocalAuth, 8 | } from '../utils/authScope'; 9 | import { runElectron } from '../utils/electron'; 10 | import { loadAsset } from '../utils/assets'; 11 | import intercom from '../utils/intercom'; 12 | import withToaster from '../components/toast/withToaster'; 13 | import toastStore from '../utils/toastStore'; 14 | import apolloPersistor from '../utils/apolloPersistor'; 15 | 16 | export const getUserQuery = apolloPersistor.instance({ 17 | name: 'query', 18 | map: ({ ...args }) => ({ 19 | ...args, 20 | query: gql` 21 | query GetUser { 22 | me { 23 | id 24 | hash 25 | name 26 | username 27 | email 28 | createdAt 29 | updatedAt 30 | isSubscribed 31 | preferences { 32 | shortcutOpen 33 | } 34 | } 35 | } 36 | `, 37 | }), 38 | }); 39 | 40 | export interface IAppProps { 41 | addToast: (data: { type?: string; contents?: string }) => any; 42 | } 43 | 44 | const App: FunctionComponent = ({ addToast }) => { 45 | const [authChecked, setAuthChecked] = useState(false); 46 | useEffect(() => { 47 | const unwatch = authScope.watch({ 48 | data: data => { 49 | authStore.dispatch.patch(data); 50 | if (Object.keys(data).length) { 51 | getUserQuery.execute(); 52 | } 53 | setAuthChecked(true); 54 | }, 55 | }); 56 | retrieveLocalAuth.execute(); 57 | return () => unwatch(); 58 | }, []); 59 | useEffect(() => { 60 | const unwatch = getUserQuery.watch({ 61 | catch: () => saveLocalAuth.execute({ data: {} }), 62 | data: ({ me }) => { 63 | if (me) { 64 | intercom.update({ 65 | user_id: me.id, 66 | user_hash: me.hash, 67 | name: me.name, 68 | username: me.username, 69 | email: me.email, 70 | created_at: me.createdAt, 71 | updated_at: me.updatedAt, 72 | is_subscribed: me.isSubscribed, 73 | }); 74 | if (me.preferences) { 75 | runElectron(electron => { 76 | electron.ipcRenderer.send('updateShortcuts', { 77 | open: me.preferences.shortcutOpen, 78 | }); 79 | }); 80 | } 81 | } 82 | }, 83 | }); 84 | runElectron(electron => electron.ipcRenderer.send('ready')); 85 | return () => unwatch(); 86 | }, []); 87 | useEffect(() => { 88 | intercom.start(); 89 | return () => intercom.shutdown(); 90 | }); 91 | useEffect(() => { 92 | const unwatch = toastStore.watch({ 93 | state: ({ type, contents }) => contents && addToast({ type, contents }), 94 | }); 95 | return () => unwatch(); 96 | }); 97 | if (!authChecked) { 98 | return ( 99 |
100 | 101 |
102 | ); 103 | } 104 | return ; 105 | }; 106 | 107 | export default withToaster(App); 108 | -------------------------------------------------------------------------------- /packages/client/src/containers/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react'; 2 | import Background from '../components/layouts/Background'; 3 | import MainRoutes from './routes/MainRoutes'; 4 | import Topbar from './menus/Topbar'; 5 | 6 | export interface IDashboardProps {} 7 | 8 | const Dashboard: FunctionComponent = () => ( 9 | 10 | 11 | 12 | 13 | ); 14 | 15 | export default Dashboard; 16 | -------------------------------------------------------------------------------- /packages/client/src/containers/menus/CodeMenu.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react'; 2 | import { Link } from 'lumbridge'; 3 | import PopupMenu from '../../components/menus/PopupMenu'; 4 | import { ICodeFragment } from '../../components/lists/ChooseCode'; 5 | 6 | export interface ICodeMenuProps { 7 | data: { 8 | code: ICodeFragment; 9 | }; 10 | handlers: { 11 | clipboardCopyCode: (data: { value: string; id?: string }) => void; 12 | deleteCode: (data: { id: string }) => void; 13 | cloneCode: (data: { id: string }) => void; 14 | }; 15 | } 16 | 17 | const CodeMenu: FunctionComponent = ({ 18 | data: { code }, 19 | handlers, 20 | }) => { 21 | const deleteCode = () => handlers.deleteCode(code); 22 | const cloneCode = () => handlers.cloneCode(code); 23 | const copyCode = () => 24 | handlers.clipboardCopyCode({ value: code.contents, id: code.id }); 25 | return ( 26 | 27 | Copy 28 | 29 | Edit 30 | 31 | Duplicate 32 | Delete 33 | 34 | ); 35 | }; 36 | 37 | export default CodeMenu; 38 | -------------------------------------------------------------------------------- /packages/client/src/containers/menus/SettingsMenu.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react'; 2 | import { Link } from 'lumbridge'; 3 | import CardMenu from '../../components/menus/CardMenu'; 4 | 5 | export interface ISettingsMenuProps {} 6 | 7 | const SettingsMenu: FunctionComponent = () => ( 8 | 9 | 10 | Profile 11 | 12 | 13 | Preferences 14 | 15 | 16 | Security 17 | 18 | {/* */} 19 | {/* Accounts */} 20 | {/* */} 21 | {/* */} 22 | {/* Membership */} 23 | {/* */} 24 | 25 | ); 26 | 27 | export default SettingsMenu; 28 | -------------------------------------------------------------------------------- /packages/client/src/containers/menus/Topbar.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, useState, useEffect } from 'react'; 2 | import { Link, Terminal } from 'lumbridge'; 3 | import Header from '../../components/menus/Header'; 4 | import PopupMenu from '../../components/menus/PopupMenu'; 5 | import { saveLocalAuth } from '../../utils/authScope'; 6 | import intercom from '../../utils/intercom'; 7 | import { runElectron } from '../../utils/electron'; 8 | 9 | export interface ITopbarProps {} 10 | 11 | const Topbar: FunctionComponent = () => { 12 | const [canUpdate, setCanUpdate] = useState(false); 13 | useEffect(() => { 14 | runElectron(electron => { 15 | electron.ipcRenderer.on( 16 | 'update', 17 | (_: any, { type }: { type: string }) => { 18 | if (type === 'downloaded') { 19 | setCanUpdate(true); 20 | } 21 | } 22 | ); 23 | }); 24 | }, []); 25 | const logout = () => { 26 | saveLocalAuth.execute({ data: {} }); 27 | intercom.restart(); 28 | setTimeout(() => Terminal.navigate('/auth')); 29 | }; 30 | const restartApp = () => { 31 | runElectron(electron => { 32 | electron.ipcRenderer.send('quitAndUpdate'); 33 | }); 34 | }; 35 | const updateItem = canUpdate && ( 36 | Install Update 37 | ); 38 | const menu = ( 39 | 40 | 41 | Profile 42 | 43 | 44 | Preferences 45 | 46 | 47 | Security 48 | 49 | {/* */} 50 | {/* Accounts */} 51 | {/* */} 52 | {/* */} 53 | {/* Membership */} 54 | {/* */} 55 | 56 | Help & Feedback 57 | 58 | {updateItem} 59 | Logout 60 | 61 | ); 62 | return
; 63 | }; 64 | 65 | export default Topbar; 66 | -------------------------------------------------------------------------------- /packages/client/src/containers/modals/CreateBundle.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, useEffect } from 'react'; 2 | import gql from 'graphql-tag'; 3 | import apolloPersistor from '../../utils/apolloPersistor'; 4 | import useInstance from '../../effects/useInstance'; 5 | import BundleForm from '../../components/forms/BundleForm'; 6 | 7 | export const createBundleMutation = apolloPersistor.instance({ 8 | name: 'mutate', 9 | map: ({ ...args }) => ({ 10 | ...args, 11 | mutation: gql` 12 | mutation CreateBundle($input: BundleInput!) { 13 | addBundle(input: $input) { 14 | id 15 | name 16 | } 17 | } 18 | `, 19 | }), 20 | }); 21 | 22 | export interface ICreateBundleProps { 23 | handlers: { 24 | choose: (...args: any[]) => any; 25 | }; 26 | } 27 | 28 | const CreateBundle: FunctionComponent = ({ 29 | handlers: bundleHandlers, 30 | }) => { 31 | const { error, loading } = useInstance(createBundleMutation); 32 | useEffect(() => { 33 | const unwatch = createBundleMutation.watch({ 34 | data: ({ addBundle }) => addBundle && bundleHandlers.choose(addBundle), 35 | }); 36 | return () => unwatch(); 37 | }); 38 | const data = { 39 | prefill: {}, 40 | error, 41 | loading, 42 | }; 43 | const handlers = { 44 | submit: ({ name, readme }: any) => 45 | createBundleMutation.execute({ 46 | variables: { 47 | input: { name, readme }, 48 | }, 49 | }), 50 | }; 51 | return ; 52 | }; 53 | 54 | export default CreateBundle; 55 | -------------------------------------------------------------------------------- /packages/client/src/containers/modals/SelectBundle.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react'; 2 | import gql from 'graphql-tag'; 3 | import apolloPersistor from '../../utils/apolloPersistor'; 4 | import useInstanceExecute from '../../effects/useInstanceExecute'; 5 | import ChooseBundle from '../../components/lists/ChooseBundle'; 6 | 7 | export const bundleListQuery = apolloPersistor.instance({ 8 | name: 'query', 9 | map: ({ ...args }) => ({ 10 | ...args, 11 | query: gql` 12 | query ListBundles { 13 | userBundles { 14 | id 15 | name 16 | } 17 | } 18 | `, 19 | }), 20 | }); 21 | 22 | export interface ISelectBundleProps { 23 | handlers: { 24 | choose: (...args: any[]) => any; 25 | }; 26 | } 27 | 28 | const SelectBundle: FunctionComponent = ({ handlers }) => { 29 | const { 30 | data: { userBundles }, 31 | error, 32 | loading, 33 | } = useInstanceExecute(bundleListQuery); 34 | const data = { 35 | bundles: userBundles || [], 36 | error, 37 | loading, 38 | }; 39 | return ; 40 | }; 41 | 42 | export default SelectBundle; 43 | -------------------------------------------------------------------------------- /packages/client/src/containers/pages/Auth.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react'; 2 | import ChooseAuth from '../../components/menus/ChooseAuth'; 3 | 4 | export interface IAuthProps {} 5 | 6 | const Auth: FunctionComponent = () => { 7 | return ; 8 | }; 9 | 10 | export default Auth; 11 | -------------------------------------------------------------------------------- /packages/client/src/containers/pages/CreateCode.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react'; 2 | import { Terminal } from 'lumbridge'; 3 | import gql from 'graphql-tag'; 4 | import CodeForm from '../../components/forms/CodeForm'; 5 | import apolloPersistor from '../../utils/apolloPersistor'; 6 | import useInstance from '../../effects/useInstance'; 7 | import useInstanceSuccess from '../../effects/useInstanceSuccess'; 8 | import { codeListQuery } from './FindCode'; 9 | 10 | export const createCodeMutation = apolloPersistor.instance({ 11 | name: 'mutate', 12 | map: ({ ...args }) => ({ 13 | ...args, 14 | mutation: gql` 15 | mutation CreateCode($input: CodeInput!) { 16 | addCode(input: $input) { 17 | id 18 | } 19 | } 20 | `, 21 | }), 22 | }); 23 | 24 | export interface ICreateCodeProps {} 25 | 26 | const CreateCode: FunctionComponent = () => { 27 | const { error, loading } = useInstance(createCodeMutation); 28 | useInstanceSuccess(createCodeMutation, () => { 29 | codeListQuery.redo(); 30 | setTimeout(() => Terminal.navigate('/')); 31 | }); 32 | const data = { 33 | prefill: {}, 34 | error, 35 | loading, 36 | title: 'Create Code', 37 | }; 38 | const handlers = { 39 | submit: ({ name, shortcut, contents }: any) => 40 | createCodeMutation.execute({ 41 | variables: { 42 | input: { name, shortcut, contents }, 43 | }, 44 | }), 45 | }; 46 | return ; 47 | }; 48 | 49 | export default CreateCode; 50 | -------------------------------------------------------------------------------- /packages/client/src/containers/pages/EditCode.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, useEffect, useState } from 'react'; 2 | import gql from 'graphql-tag'; 3 | import queryString from 'query-string'; 4 | import CodeForm from '../../components/forms/CodeForm'; 5 | import apolloPersistor from '../../utils/apolloPersistor'; 6 | import useInstanceSuccess from '../../effects/useInstanceSuccess'; 7 | import useInstance from '../../effects/useInstance'; 8 | import { Terminal } from 'lumbridge'; 9 | 10 | export const getCodeQuery = apolloPersistor.instance({ 11 | name: 'query', 12 | map: ({ ...args }) => ({ 13 | ...args, 14 | query: gql` 15 | query GetCode($id: String!) { 16 | code(id: $id) { 17 | id 18 | name 19 | shortcut 20 | contents 21 | } 22 | } 23 | `, 24 | }), 25 | }); 26 | 27 | export const editCodeMutation = apolloPersistor.instance({ 28 | name: 'mutate', 29 | map: ({ ...args }) => ({ 30 | ...args, 31 | mutation: gql` 32 | mutation EditCode($id: String!, $input: CodeInput!) { 33 | editCode(id: $id, input: $input) { 34 | id 35 | } 36 | } 37 | `, 38 | }), 39 | }); 40 | 41 | export interface IEditCodeProps {} 42 | 43 | const EditCode: FunctionComponent = () => { 44 | const [code, setCode] = useState(null); 45 | useEffect(() => { 46 | const unwatch = getCodeQuery.watch({ 47 | data: ({ code: prefill }) => setCode(prefill), 48 | }); 49 | return () => unwatch(); 50 | }, []); 51 | useEffect(() => { 52 | const { id } = queryString.parse(window.location.search); 53 | if (!id) { 54 | setTimeout(() => Terminal.navigate('/')); 55 | } 56 | getCodeQuery.execute({ variables: { id } }); 57 | }, []); 58 | const { error, loading } = useInstance(editCodeMutation); 59 | useInstanceSuccess(editCodeMutation); 60 | const data = { 61 | prefill: code, 62 | error, 63 | loading, 64 | title: 'Edit Code', 65 | nobundle: true, 66 | }; 67 | const handlers = { 68 | submit: ({ name, shortcut, contents }: any) => 69 | editCodeMutation.execute({ 70 | variables: { 71 | id: code.id, 72 | input: { name, shortcut, contents }, 73 | }, 74 | }), 75 | }; 76 | return !code ? null : ; 77 | }; 78 | 79 | export default EditCode; 80 | -------------------------------------------------------------------------------- /packages/client/src/containers/pages/ErrorCatch.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, ReactNode } from 'react'; 2 | import * as Sentry from '@sentry/browser'; 3 | import config from '../../config'; 4 | import Problem from '../../components/forms/Problem'; 5 | import { runElectron } from '../../utils/electron'; 6 | 7 | export interface IErrorCatchProps { 8 | children: ReactNode; 9 | } 10 | 11 | export interface IErrorCatchState { 12 | error: any; 13 | info: any; 14 | } 15 | 16 | export default class ErrorCapture extends Component< 17 | IErrorCatchProps, 18 | IErrorCatchState 19 | > { 20 | constructor(props: IErrorCatchProps) { 21 | super(props); 22 | this.state = { 23 | error: null, 24 | info: null, 25 | }; 26 | } 27 | 28 | public componentDidCatch(error: any, info: any) { 29 | if (!config.debug) { 30 | this.setState({ error, info }); 31 | } 32 | } 33 | 34 | public render() { 35 | if (this.state.error) { 36 | const handlers = { 37 | submit: this.submit, 38 | }; 39 | const data = { 40 | loading: false, 41 | }; 42 | return ; 43 | } 44 | return this.props.children; 45 | } 46 | 47 | private submit = ({ message }: { message: string }) => { 48 | const { error, info } = this.state; 49 | Sentry.withScope(scope => { 50 | scope.setExtra('message', message); 51 | Object.keys(info).forEach(key => scope.setExtra(key, info[key])); 52 | Sentry.captureException(error); 53 | }); 54 | runElectron(electron => { 55 | electron.ipcRenderer.send('relaunch'); 56 | }); 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /packages/client/src/containers/pages/Login.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, useEffect } from 'react'; 2 | import gql from 'graphql-tag'; 3 | import { Terminal } from 'lumbridge'; 4 | import LoginForm from '../../components/forms/LoginForm'; 5 | import apolloPersistor from '../../utils/apolloPersistor'; 6 | import useInstance from '../../effects/useInstance'; 7 | import { saveLocalAuth } from '../../utils/authScope'; 8 | 9 | export const loginMutation = apolloPersistor.instance({ 10 | name: 'mutate', 11 | map: ({ ...args }) => ({ 12 | ...args, 13 | mutation: gql` 14 | mutation Login($username: String!, $password: String!) { 15 | authLoginCustom(username: $username, password: $password) { 16 | token 17 | userId 18 | } 19 | } 20 | `, 21 | }), 22 | }); 23 | 24 | export interface ILoginProps {} 25 | 26 | const Login: FunctionComponent = () => { 27 | const { error, loading } = useInstance(loginMutation); 28 | useEffect(() => { 29 | const unmount = loginMutation.watch({ 30 | data: ({ authLoginCustom }) => { 31 | saveLocalAuth.execute({ data: authLoginCustom }); 32 | setTimeout(() => Terminal.navigate('/')); 33 | }, 34 | }); 35 | return () => unmount(); 36 | }, []); 37 | const data = { loading, error }; 38 | const handlers = { 39 | submit: (formData: any) => loginMutation.execute({ variables: formData }), 40 | }; 41 | return ; 42 | }; 43 | 44 | export default Login; 45 | -------------------------------------------------------------------------------- /packages/client/src/containers/pages/Market.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react'; 2 | import gql from 'graphql-tag'; 3 | import Marketplace from '../../components/layouts/Marketplace'; 4 | import apolloPersistor from '../../utils/apolloPersistor'; 5 | import useInstanceExecute from '../../effects/useInstanceExecute'; 6 | 7 | export const getMarketBundles = apolloPersistor.instance({ 8 | name: 'query', 9 | map: ({ ...args }) => ({ 10 | ...args, 11 | query: gql` 12 | query GetMarketBundles { 13 | marketBundles { 14 | id 15 | name 16 | readme 17 | } 18 | } 19 | `, 20 | }), 21 | }); 22 | 23 | export interface IMarketProps {} 24 | 25 | const Market: FunctionComponent = () => { 26 | const { 27 | data: { marketBundles }, 28 | error, 29 | loading, 30 | } = useInstanceExecute(getMarketBundles); 31 | const data = { 32 | bundles: marketBundles || [], 33 | }; 34 | const handlers = { 35 | subscribe: () => { 36 | // TODO: sign up a person to this bundle... 37 | }, 38 | }; 39 | return ; 40 | }; 41 | 42 | export default Market; 43 | -------------------------------------------------------------------------------- /packages/client/src/containers/pages/Settings.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react'; 2 | import Cape from '../../components/layouts/Cape'; 3 | import SettingsMenu from '../menus/SettingsMenu'; 4 | import SettingsRoutes from '../routes/SettingsRoutes'; 5 | 6 | export interface ISettingsProps {} 7 | 8 | const Settings: FunctionComponent = () => { 9 | return ( 10 | 11 | 12 | 13 | 14 | ); 15 | }; 16 | 17 | export default Settings; 18 | -------------------------------------------------------------------------------- /packages/client/src/containers/pages/SignUp.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, useEffect } from 'react'; 2 | import gql from 'graphql-tag'; 3 | import { Terminal } from 'lumbridge'; 4 | import SignUpForm from '../../components/forms/SignUpForm'; 5 | import apolloPersistor from '../../utils/apolloPersistor'; 6 | import useInstance from '../../effects/useInstance'; 7 | import { saveLocalAuth } from '../../utils/authScope'; 8 | 9 | export const signUpMutation = apolloPersistor.instance({ 10 | name: 'mutate', 11 | map: ({ ...args }) => ({ 12 | ...args, 13 | mutation: gql` 14 | mutation SignUp($username: String!, $password: String!, $email: String!) { 15 | authCreateCustom( 16 | username: $username 17 | password: $password 18 | email: $email 19 | ) { 20 | token 21 | userId 22 | } 23 | } 24 | `, 25 | }), 26 | }); 27 | 28 | export interface ISignUpProps {} 29 | 30 | const SignUp: FunctionComponent = () => { 31 | const { error, loading } = useInstance(signUpMutation); 32 | useEffect(() => { 33 | const unwatch = signUpMutation.watch({ 34 | data: ({ authCreateCustom }) => { 35 | saveLocalAuth.execute({ data: authCreateCustom }); 36 | setTimeout(() => Terminal.navigate('/')); 37 | }, 38 | }); 39 | return () => unwatch(); 40 | }, []); 41 | const data = { 42 | loading, 43 | error, 44 | }; 45 | const handlers = { 46 | submit: (formData: any) => signUpMutation.execute({ variables: formData }), 47 | }; 48 | return ; 49 | }; 50 | 51 | export default SignUp; 52 | -------------------------------------------------------------------------------- /packages/client/src/containers/routes/AuthRoutes.tsx: -------------------------------------------------------------------------------- 1 | import { Router } from 'lumbridge'; 2 | import Dashboard from '../Dashboard'; 3 | import Auth from '../pages/Auth'; 4 | import Login from '../pages/Login'; 5 | import SignUp from '../pages/SignUp'; 6 | 7 | export default Router.create({ 8 | routes: { 9 | auth: { 10 | path: '/auth', 11 | component: Auth, 12 | }, 13 | login: { 14 | path: '/login', 15 | component: Login, 16 | }, 17 | signUp: { 18 | path: '/sign-up', 19 | component: SignUp, 20 | }, 21 | app: { 22 | path: '/', 23 | component: Dashboard, 24 | }, 25 | }, 26 | }).render(); 27 | -------------------------------------------------------------------------------- /packages/client/src/containers/routes/MainRoutes.tsx: -------------------------------------------------------------------------------- 1 | import { Router } from 'lumbridge'; 2 | import FindCode from '../pages/FindCode'; 3 | import CreateCode from '../pages/CreateCode'; 4 | import EditCode from '../pages/EditCode'; 5 | import Settings from '../pages/Settings'; 6 | import authStore from '../../utils/authStore'; 7 | 8 | export default Router.create({ 9 | nomatch: { 10 | redirect: '/auth', 11 | }, 12 | change: { 13 | before: () => authStore.state.loggedIn, 14 | }, 15 | routes: { 16 | dashboard: { 17 | path: '/', 18 | exact: true, 19 | component: FindCode, 20 | }, 21 | createCode: { 22 | path: '/create', 23 | component: CreateCode, 24 | }, 25 | editCode: { 26 | path: '/edit', 27 | component: EditCode, 28 | }, 29 | settings: { 30 | path: '/settings', 31 | component: Settings, 32 | }, 33 | }, 34 | }).render(); 35 | -------------------------------------------------------------------------------- /packages/client/src/containers/routes/SettingsRoutes.tsx: -------------------------------------------------------------------------------- 1 | import { Router } from 'lumbridge'; 2 | import Accounts from '../settings/Accounts'; 3 | import Membership from '../settings/Membership'; 4 | import Profile from '../settings/Profile'; 5 | import Preferences from '../settings/Preferences'; 6 | import Security from '../settings/Security'; 7 | 8 | export default Router.create({ 9 | base: '/settings', 10 | routes: { 11 | profile: { 12 | path: '/profile', 13 | component: Profile, 14 | }, 15 | preferences: { 16 | path: '/preferences', 17 | component: Preferences, 18 | }, 19 | security: { 20 | path: '/security', 21 | component: Security, 22 | }, 23 | accounts: { 24 | path: '/accounts', 25 | component: Accounts, 26 | }, 27 | membership: { 28 | path: '/membership', 29 | component: Membership, 30 | }, 31 | }, 32 | }).render(); 33 | -------------------------------------------------------------------------------- /packages/client/src/containers/settings/Accounts.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, useEffect, useState } from 'react'; 2 | import gql from 'graphql-tag'; 3 | import queryString from 'query-string'; 4 | import List from '../../components/layouts/List'; 5 | import Title from '../../components/texts/Title'; 6 | import Control from '../../components/inputs/Control'; 7 | import Button from '../../components/buttons/Button'; 8 | import apolloPersistor from '../../utils/apolloPersistor'; 9 | import useInstanceExecute from '../../effects/useInstanceExecute'; 10 | import { runElectron } from '../../utils/electron'; 11 | 12 | export const githubUrlQuery = apolloPersistor.instance({ 13 | name: 'query', 14 | map: ({ ...args }) => ({ 15 | ...args, 16 | query: gql` 17 | query GitHubUrl { 18 | oauthGitHubUrl 19 | userConnectedGitHub 20 | } 21 | `, 22 | }), 23 | }); 24 | 25 | export const githubConnectMutation = apolloPersistor.instance({ 26 | name: 'mutate', 27 | map: ({ ...args }) => ({ 28 | ...args, 29 | mutation: gql` 30 | mutation ConnectGitHub($code: String!) { 31 | authConnectGitHub(code: $code) { 32 | token 33 | userId 34 | } 35 | } 36 | `, 37 | }), 38 | }); 39 | 40 | export interface IAccountsProps {} 41 | 42 | const Accounts: FunctionComponent = () => { 43 | useEffect(() => { 44 | const { code } = queryString.parse(window.location.search); 45 | if (code) { 46 | githubConnectMutation.execute({ variables: { code } }); 47 | } 48 | }, []); 49 | const { 50 | data: { oauthGitHubUrl, userConnectedGitHub }, 51 | } = useInstanceExecute(githubUrlQuery); 52 | const navigateGitHub = () => { 53 | runElectron( 54 | electron => { 55 | electron.ipcRenderer.send('authGitHub', oauthGitHubUrl); 56 | }, 57 | () => window.location.assign(oauthGitHubUrl) 58 | ); 59 | }; 60 | const GitHub = ({ ...args }) => ( 61 | 64 | ); 65 | return ( 66 | 67 | Accounts 68 |
69 | 70 | 76 | 77 |
78 | ); 79 | }; 80 | 81 | export default Accounts; 82 | -------------------------------------------------------------------------------- /packages/client/src/containers/settings/Membership.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react'; 2 | import { Elements } from 'react-stripe-elements'; 3 | import gql from 'graphql-tag'; 4 | import List from '../../components/layouts/List'; 5 | import Title from '../../components/texts/Title'; 6 | import CardForm from '../../components/forms/CardForm'; 7 | import apolloPersistor from '../../utils/apolloPersistor'; 8 | import useInstance from '../../effects/useInstance'; 9 | import useInstanceSuccess from '../../effects/useInstanceSuccess'; 10 | 11 | export const createMembershipMutation = apolloPersistor.instance({ 12 | name: 'mutate', 13 | map: ({ ...args }) => ({ 14 | ...args, 15 | mutation: gql` 16 | mutation CreateMembership($token: String!) { 17 | subscribeUser(token: $token) { 18 | id 19 | } 20 | } 21 | `, 22 | }), 23 | }); 24 | 25 | export interface IMembershipProps {} 26 | 27 | const Membership: FunctionComponent = () => { 28 | const { loading, error } = useInstance(createMembershipMutation); 29 | useInstanceSuccess(createMembershipMutation); 30 | const data = { 31 | prefill: { 32 | name: '', 33 | }, 34 | loading, 35 | error, 36 | }; 37 | const handlers = { 38 | submit: ({ token }: { token: string }) => 39 | createMembershipMutation.execute({ variables: { token } }), 40 | }; 41 | return ( 42 | 43 | Membership 44 |
45 | 46 | 47 | 48 |
49 | ); 50 | }; 51 | 52 | export default Membership; 53 | -------------------------------------------------------------------------------- /packages/client/src/containers/settings/Preferences.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, useEffect } from 'react'; 2 | import gql from 'graphql-tag'; 3 | import PreferencesForm from '../../components/forms/PreferencesForm'; 4 | import List from '../../components/layouts/List'; 5 | import Title from '../../components/texts/Title'; 6 | import apolloPersistor from '../../utils/apolloPersistor'; 7 | import useInstanceSuccess from '../../effects/useInstanceSuccess'; 8 | import useInstance from '../../effects/useInstance'; 9 | import useInstanceExecute from '../../effects/useInstanceExecute'; 10 | import { runElectron } from '../../utils/electron'; 11 | 12 | export const getUserQuery = apolloPersistor.instance({ 13 | name: 'query', 14 | map: ({ ...args }) => ({ 15 | ...args, 16 | query: gql` 17 | query GetUser { 18 | me { 19 | id 20 | preferences { 21 | shortcutOpen 22 | } 23 | } 24 | } 25 | `, 26 | }), 27 | }); 28 | 29 | export const editPreferencesMutation = apolloPersistor.instance({ 30 | name: 'mutate', 31 | map: ({ ...args }) => ({ 32 | ...args, 33 | mutation: gql` 34 | mutation EditUserPreferences($input: UserInput!) { 35 | editMe(input: $input) { 36 | id 37 | } 38 | } 39 | `, 40 | }), 41 | }); 42 | 43 | export interface IPreferencesProps {} 44 | 45 | const Preferences: FunctionComponent = () => { 46 | const { 47 | data: { me }, 48 | } = useInstanceExecute(getUserQuery); 49 | const { loading, error } = useInstance(editPreferencesMutation); 50 | useInstanceSuccess(editPreferencesMutation); 51 | useEffect(() => { 52 | const unwatch = editPreferencesMutation.watch({ 53 | data: () => getUserQuery.execute(), 54 | }); 55 | const unwatchUser = getUserQuery.watch({ 56 | data: ({ me: user }) => { 57 | if (user && user.preferences) { 58 | runElectron(electron => { 59 | electron.ipcRenderer.send('updateShortcuts', { 60 | open: user.preferences.shortcutOpen, 61 | }); 62 | }); 63 | } 64 | }, 65 | }); 66 | return () => { 67 | unwatch(); 68 | unwatchUser(); 69 | }; 70 | }, []); 71 | const data = { 72 | prefill: me && me.preferences ? me.preferences : {}, 73 | loading, 74 | error, 75 | }; 76 | const handlers = { 77 | submit: ({ shortcutOpen }: any) => 78 | editPreferencesMutation.execute({ 79 | variables: { input: { preferences: { shortcutOpen } } }, 80 | }), 81 | }; 82 | return ( 83 | 84 | Preferences 85 |
86 | {me && } 87 |
88 | ); 89 | }; 90 | 91 | export default Preferences; 92 | -------------------------------------------------------------------------------- /packages/client/src/containers/settings/Profile.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react'; 2 | import gql from 'graphql-tag'; 3 | import ProfileForm from '../../components/forms/ProfileForm'; 4 | import List from '../../components/layouts/List'; 5 | import Title from '../../components/texts/Title'; 6 | import apolloPersistor from '../../utils/apolloPersistor'; 7 | import useInstanceSuccess from '../../effects/useInstanceSuccess'; 8 | import useInstance from '../../effects/useInstance'; 9 | import useInstanceExecute from '../../effects/useInstanceExecute'; 10 | 11 | export const getUserQuery = apolloPersistor.instance({ 12 | name: 'query', 13 | map: ({ ...args }) => ({ 14 | ...args, 15 | query: gql` 16 | query GetUser { 17 | me { 18 | id 19 | name 20 | email 21 | } 22 | } 23 | `, 24 | }), 25 | }); 26 | 27 | export const editUserMutation = apolloPersistor.instance({ 28 | name: 'mutate', 29 | map: ({ ...args }) => ({ 30 | ...args, 31 | mutation: gql` 32 | mutation EditUser($input: UserInput!) { 33 | editMe(input: $input) { 34 | id 35 | } 36 | } 37 | `, 38 | }), 39 | }); 40 | 41 | export interface IProfileProps {} 42 | 43 | const Profile: FunctionComponent = () => { 44 | const { 45 | data: { me }, 46 | } = useInstanceExecute(getUserQuery); 47 | const { loading, error } = useInstance(editUserMutation); 48 | useInstanceSuccess(editUserMutation); 49 | const data = { 50 | prefill: me, 51 | loading, 52 | error, 53 | }; 54 | const handlers = { 55 | submit: ({ name, email }: any) => 56 | editUserMutation.execute({ variables: { input: { name, email } } }), 57 | }; 58 | return ( 59 | 60 | Profile 61 |
62 | {me && } 63 |
64 | ); 65 | }; 66 | 67 | export default Profile; 68 | -------------------------------------------------------------------------------- /packages/client/src/containers/settings/Security.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react'; 2 | import gql from 'graphql-tag'; 3 | import PasswordForm from '../../components/forms/PasswordForm'; 4 | import List from '../../components/layouts/List'; 5 | import Title from '../../components/texts/Title'; 6 | import apolloPersistor from '../../utils/apolloPersistor'; 7 | import useInstanceSuccess from '../../effects/useInstanceSuccess'; 8 | import useInstance from '../../effects/useInstance'; 9 | 10 | export const updateUserPasswordMutation = apolloPersistor.instance({ 11 | name: 'mutate', 12 | map: ({ ...args }) => ({ 13 | ...args, 14 | mutation: gql` 15 | mutation UpdatePassword($oldPassword: String!, $newPassword: String!) { 16 | authPasswordChangeCustom( 17 | oldPassword: $oldPassword 18 | newPassword: $newPassword 19 | ) { 20 | token 21 | userId 22 | } 23 | } 24 | `, 25 | }), 26 | }); 27 | 28 | export interface ISecurityProps {} 29 | 30 | const Security: FunctionComponent = () => { 31 | const { loading, error } = useInstance(updateUserPasswordMutation); 32 | useInstanceSuccess(updateUserPasswordMutation); 33 | const data = { 34 | prefill: {}, 35 | loading, 36 | error, 37 | }; 38 | const handlers = { 39 | submit: ({ oldPassword, newPassword }: any) => 40 | updateUserPasswordMutation.execute({ 41 | variables: { oldPassword, newPassword }, 42 | }), 43 | }; 44 | return ( 45 | 46 | Security 47 |
48 | 49 |
50 | ); 51 | }; 52 | 53 | export default Security; 54 | -------------------------------------------------------------------------------- /packages/client/src/effects/useInstance.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { Instance } from 'lumbridge'; 3 | import toastStore from '../utils/toastStore'; 4 | 5 | export default (instance: Instance) => { 6 | const [error, setError] = useState(null); 7 | const [loading, setLoading] = useState(false); 8 | useEffect( 9 | () => { 10 | const unwatch = instance.watch({ 11 | catch: issue => { 12 | setError(issue || null); 13 | toastStore.dispatch.ping({ 14 | type: 'error', 15 | contents: 16 | issue && issue.message 17 | ? issue.message 18 | : 'There was an error talking to the server.', 19 | }); 20 | }, 21 | status: status => setLoading(status.loading), 22 | }); 23 | return () => unwatch(); 24 | }, 25 | [instance] 26 | ); 27 | return { loading, error }; 28 | }; 29 | -------------------------------------------------------------------------------- /packages/client/src/effects/useInstanceExecute.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { Instance } from 'lumbridge'; 3 | 4 | export default (instance: Instance) => { 5 | const [error, setError] = useState(null); 6 | const [loading, setLoading] = useState(false); 7 | const [data, setData] = useState<{ [name: string]: any }>({}); 8 | useEffect( 9 | () => { 10 | const unwatch = instance.watch({ 11 | data: results => setData(results), 12 | catch: issue => setError(issue || null), 13 | status: status => setLoading(status.loading), 14 | }); 15 | instance.execute(); 16 | return () => unwatch(); 17 | }, 18 | [instance] 19 | ); 20 | return { data, loading, error }; 21 | }; 22 | -------------------------------------------------------------------------------- /packages/client/src/effects/useInstanceSuccess.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { Instance } from 'lumbridge'; 3 | import toastStore from '../utils/toastStore'; 4 | 5 | export default (instance: Instance, cb?: (data: any) => any) => { 6 | useEffect( 7 | () => { 8 | const unwatch = instance.watch({ 9 | data: data => { 10 | toastStore.dispatch.ping({ 11 | type: '', 12 | contents: 'Saved successfully.', 13 | }); 14 | if (cb) { 15 | cb(data); 16 | } 17 | }, 18 | }); 19 | return () => unwatch(); 20 | }, 21 | [instance] 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /packages/client/src/effects/useKeyboard.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | export default (keycode: string) => { 4 | const [activeKey, setActiveKey] = useState(null); 5 | useEffect(() => { 6 | const keyEvent = (event: KeyboardEvent) => setActiveKey(event); 7 | window.addEventListener(keycode, keyEvent as any); 8 | return () => window.removeEventListener(keycode, keyEvent as any); 9 | }, []); 10 | return { activeKey }; 11 | }; 12 | -------------------------------------------------------------------------------- /packages/client/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | height: 100vh; 5 | width: 100%; 6 | display: flex; 7 | box-sizing: border-box; 8 | position: relative; 9 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 10 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 11 | sans-serif; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | code { 17 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 18 | monospace; 19 | } 20 | 21 | form { 22 | flex-grow: 1; 23 | display: flex; 24 | } 25 | 26 | #root { 27 | width: 100%; 28 | flex-grow: 1; 29 | display: flex; 30 | } 31 | 32 | *::-webkit-scrollbar { 33 | width: 6px; 34 | } 35 | *::-webkit-scrollbar-thumb { 36 | background-color: rgba(0, 0, 0, 0.2); 37 | border-radius: 2px; 38 | } -------------------------------------------------------------------------------- /packages/client/src/index.tsx: -------------------------------------------------------------------------------- 1 | import './clean.css'; 2 | import './index.css'; 3 | 4 | import React from 'react'; 5 | import ReactDOM from 'react-dom'; 6 | import * as Sentry from '@sentry/browser'; 7 | import { StripeProvider } from 'react-stripe-elements'; 8 | import * as serviceWorker from './serviceWorker'; 9 | import config from './config'; 10 | import ErrorCatch from './containers/pages/ErrorCatch'; 11 | import App from './containers/App'; 12 | import Toaster from './components/toast/Toaster'; 13 | 14 | /** 15 | * Register error reporter before app rendering starts. 16 | */ 17 | Sentry.init({ 18 | dsn: config.sentryDSN, 19 | environment: config.environment, 20 | }); 21 | 22 | const app = ( 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | 32 | ReactDOM.render(app, document.getElementById('root')); 33 | 34 | /** 35 | * If you want your app to work offline and load faster, you can change 36 | * unregister() to register() below. Note this comes with some pitfalls. 37 | * Learn more about service workers: http://bit.ly/CRA-PWA 38 | */ 39 | serviceWorker.unregister(); 40 | -------------------------------------------------------------------------------- /packages/client/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/client/src/styles/animate.ts: -------------------------------------------------------------------------------- 1 | import { keyframes } from 'styled-components'; 2 | 3 | export default { 4 | /** 5 | * Fade in to view. 6 | */ 7 | fadeIn: keyframes` 8 | from { 9 | opacity: 0; 10 | } 11 | to { 12 | opacity: 1; 13 | } 14 | `, 15 | /** 16 | * Fade out of view. 17 | */ 18 | fadeOut: keyframes` 19 | from { 20 | opacity: 1; 21 | } 22 | to { 23 | opacity: 0; 24 | } 25 | `, 26 | /** 27 | * Slide in from left. 28 | */ 29 | slideRight: keyframes` 30 | from { 31 | transform: translateX(-100%); 32 | } 33 | to { 34 | transform: translateX(0); 35 | } 36 | `, 37 | /** 38 | * Slide in from left. 39 | */ 40 | slideLeft: keyframes` 41 | from { 42 | transform: translateX(100%); 43 | } 44 | to { 45 | transform: translateX(0); 46 | } 47 | `, 48 | }; 49 | -------------------------------------------------------------------------------- /packages/client/src/styles/bgs.ts: -------------------------------------------------------------------------------- 1 | import Color from 'color'; 2 | import colors from './colors'; 3 | import { css } from 'styled-components'; 4 | 5 | export interface IBgsConfig { 6 | [name: string]: any; 7 | } 8 | 9 | export default { 10 | fade: () => css` 11 | color: ${colors.white}; 12 | background-color: #3e3e3e; 13 | background: linear-gradient(to top right, #191919, #3e3e3e); 14 | `, 15 | fadeLight: () => css` 16 | color: ${colors.night}; 17 | background-color: ${colors.white}; 18 | background: linear-gradient( 19 | to top right, 20 | ${colors.offset}, 21 | ${colors.white} 22 | ); 23 | `, 24 | /** 25 | * Regular components used on most pages. 26 | */ 27 | darker: ({ borderless }: IBgsConfig = {}) => css` 28 | color: ${colors.white}; 29 | background-color: ${colors.nightDarker}; 30 | ${!borderless && `border: 1px solid ${colors.nightDark};`} 31 | `, 32 | dark: ({ borderless }: IBgsConfig = {}) => css` 33 | color: ${colors.white}; 34 | background-color: ${colors.night}; 35 | ${!borderless && `border: 1px solid ${colors.nightDark};`} 36 | `, 37 | darkLight: ({ borderless }: IBgsConfig = {}) => css` 38 | color: ${colors.white}; 39 | background-color: ${colors.nightLight}; 40 | ${!borderless && `border: 1px solid ${colors.nightDark};`} 41 | `, 42 | darkLighter: ({ borderless }: IBgsConfig = {}) => css` 43 | color: ${colors.white}; 44 | background-color: ${colors.nightLighter}; 45 | ${!borderless && `border: 1px solid ${colors.nightDark};`} 46 | `, 47 | /** 48 | * Primary actions. 49 | */ 50 | marine: ({ borderless }: IBgsConfig = {}) => css` 51 | color: ${colors.marineLight}; 52 | background-color: ${colors.marine}; 53 | ${!borderless && `border: 1px solid ${colors.marineDark};`} 54 | `, 55 | marineLight: ({ borderless }: IBgsConfig = {}) => css` 56 | color: ${colors.white}; 57 | background-color: ${Color(colors.marine) 58 | .lighten(0.3) 59 | .string()}; 60 | ${!borderless && `border: 1px solid ${colors.marineDark};`} 61 | `, 62 | marineEdges: () => css` 63 | border: 1px solid ${colors.marineLight}; 64 | `, 65 | /** 66 | * Dangerous actions. 67 | */ 68 | danger: ({ borderless }: IBgsConfig = {}) => css` 69 | color: ${colors.danger}; 70 | background-color: ${colors.dangerLight}; 71 | ${!borderless && `border: 1px solid ${colors.dangerDark};`} 72 | `, 73 | dangerLight: ({ borderless }: IBgsConfig = {}) => css` 74 | color: ${colors.danger}; 75 | background-color: ${Color(colors.dangerLight) 76 | .lighten(0.3) 77 | .string()}; 78 | ${!borderless && `border: 1px solid ${colors.dangerDark};`} 79 | `, 80 | }; 81 | -------------------------------------------------------------------------------- /packages/client/src/styles/colors.ts: -------------------------------------------------------------------------------- 1 | import Color from 'color'; 2 | 3 | export default { 4 | /** 5 | * Used for card backgrounds and for composing the dark theme. 6 | */ 7 | 8 | night: '#1F1F1F', 9 | nightDark: '#101010', 10 | nightDarker: Color('#101010') 11 | .darken(0.3) 12 | .string(), 13 | nightLight: '#2F2F2F', 14 | nightLighter: Color('#2F2F2F') 15 | .lighten(0.3) 16 | .string(), 17 | /** 18 | * Used as a faint compliment to the dark theme e.g. secondary fonts. 19 | */ 20 | shade: '#797979', 21 | offset: '#eeeeee', 22 | offsetDark: '#dadada', 23 | /** 24 | * Used to determine primary actions such as buttons. 25 | */ 26 | marine: '#1836B4', 27 | marineDark: '#060D2B', 28 | marineLight: '#5D95FF', 29 | /** 30 | * Things which delete or end other things. 31 | */ 32 | danger: '#5D0707', 33 | dangerDark: '#2B0606', 34 | dangerLight: '#912C2C', 35 | dangerLighter: '#DE281B', 36 | /** 37 | * Stock colors. 38 | */ 39 | white: '#FFFFFF', 40 | black: '#000000', 41 | }; 42 | -------------------------------------------------------------------------------- /packages/client/src/styles/layouts.ts: -------------------------------------------------------------------------------- 1 | import { css } from 'styled-components'; 2 | 3 | export default { 4 | /** 5 | * Top to bottom. 6 | */ 7 | columns: () => css` 8 | display: flex; 9 | flex-direction: column; 10 | `, 11 | /** 12 | * Left to right. 13 | */ 14 | rows: () => css` 15 | display: flex; 16 | flex-direction: row; 17 | `, 18 | /** 19 | * Left to right centered. 20 | */ 21 | rowsCenter: () => css` 22 | display: flex; 23 | flex-direction: row; 24 | align-items: center; 25 | `, 26 | /** 27 | * List items. 28 | */ 29 | space: ({ space }: { space?: string | string[]; [props: string]: any }) => 30 | space && 31 | css` 32 | ${space.indexOf('top') !== -1 && 'margin-top: 10px;'} 33 | ${space.indexOf('bottom') !== -1 && 'margin-bottom: 10px;'} 34 | ${space.indexOf('left') !== -1 && 'margin-left: 10px;'} 35 | ${space.indexOf('right') !== -1 && 'margin-right: 10px;'} 36 | `, 37 | /** 38 | * Center all children. 39 | */ 40 | center: () => css` 41 | display: flex; 42 | justify-content: center; 43 | align-items: center; 44 | `, 45 | /** 46 | * Fill the entire area. 47 | */ 48 | spider: () => css` 49 | position: fixed; 50 | top: 0; 51 | right: 0; 52 | bottom: 0; 53 | left: 0; 54 | `, 55 | /** 56 | * Don't shrink when flexed. 57 | */ 58 | noshrink: () => css` 59 | flex-shrink: 0; 60 | `, 61 | }; 62 | -------------------------------------------------------------------------------- /packages/client/src/styles/shadows.ts: -------------------------------------------------------------------------------- 1 | import { css } from 'styled-components'; 2 | 3 | export default { 4 | /** 5 | * Light shadow used on most dashboard components. 6 | */ 7 | none: () => css` 8 | box-shadow: none; 9 | `, 10 | /** 11 | * Light shadow used on most dashboard components. 12 | */ 13 | simple: () => css` 14 | box-shadow: 0 1px 5px rgba(0, 0, 0, 0.25); 15 | `, 16 | /** 17 | * Light shadow used on most dashboard components. 18 | */ 19 | pop: () => css` 20 | box-shadow: 0 1px 15px 1px rgba(0, 0, 0, 0.35); 21 | `, 22 | }; 23 | -------------------------------------------------------------------------------- /packages/client/src/styles/shapes.ts: -------------------------------------------------------------------------------- 1 | import { css } from 'styled-components'; 2 | 3 | export default { 4 | /** 5 | * Used for most regular info components. 6 | */ 7 | padded: () => css` 8 | padding: 15px; 9 | `, 10 | /** 11 | * Used for most regular info components. 12 | */ 13 | thin: () => css` 14 | padding: 11px 15px; 15 | `, 16 | /** 17 | * Used for most regular info components. 18 | */ 19 | simple: () => css` 20 | padding: 15px; 21 | border-radius: 5px; 22 | `, 23 | /** 24 | * Used for most regular info components. 25 | */ 26 | narrow: () => css` 27 | padding: 11px 15px; 28 | border-radius: 5px; 29 | `, 30 | /** 31 | * Used for most regular info components. 32 | */ 33 | mini: () => css` 34 | padding: 5px 10px; 35 | border-radius: 3px; 36 | `, 37 | /** 38 | * Fill space. 39 | */ 40 | fill: () => css` 41 | width: 100%; 42 | `, 43 | }; 44 | -------------------------------------------------------------------------------- /packages/client/src/styles/states.ts: -------------------------------------------------------------------------------- 1 | import { css, FlattenInterpolation } from 'styled-components'; 2 | 3 | export default { 4 | /** 5 | * Hover with a transition. 6 | */ 7 | hovered: (hoverCss: any) => () => css` 8 | transition: 0.2s; 9 | cursor: pointer; 10 | &:hover { 11 | ${hoverCss} 12 | } 13 | `, 14 | /** 15 | * Mouse held down with transition. 16 | */ 17 | clicked: (hoverCss: any) => () => css` 18 | transition: 0.2s; 19 | &:active { 20 | ${hoverCss} 21 | } 22 | `, 23 | /** 24 | * Focus with a transition. 25 | */ 26 | focused: (hoverCss: any) => () => css` 27 | transition: 0.2s; 28 | &:focus { 29 | ${hoverCss} 30 | } 31 | `, 32 | }; 33 | -------------------------------------------------------------------------------- /packages/client/src/styles/words.ts: -------------------------------------------------------------------------------- 1 | import { css } from 'styled-components'; 2 | import colors from './colors'; 3 | 4 | export default { 5 | /** 6 | * Regular text throughout the app. 7 | */ 8 | normal: () => css` 9 | font-size: 1em; 10 | `, 11 | /** 12 | * Big titles. 13 | */ 14 | title: () => css` 15 | font-size: 1.4em; 16 | `, 17 | /** 18 | * Smaller text areas. 19 | */ 20 | small: () => css` 21 | font-size: 0.9em; 22 | `, 23 | /** 24 | * Bad things... 25 | */ 26 | danger: () => css` 27 | color: ${colors.dangerLighter}; 28 | `, 29 | /** 30 | * Bold white text. 31 | */ 32 | primary: () => css` 33 | color: ${colors.white}; 34 | `, 35 | /** 36 | * Shaded text for secondary items. 37 | */ 38 | secondary: () => css` 39 | color: ${colors.shade}; 40 | `, 41 | /** 42 | * For multi-line areas. 43 | */ 44 | multiline: () => css` 45 | line-height: 1.4em; 46 | `, 47 | }; 48 | -------------------------------------------------------------------------------- /packages/client/src/utils/apolloPersistor.ts: -------------------------------------------------------------------------------- 1 | import { Persistor } from 'lumbridge'; 2 | import { string, object } from 'yup'; 3 | import client from '../client'; 4 | 5 | const apolloPersistor: Persistor = Persistor.create({ 6 | methods: { 7 | query: { 8 | payload: { 9 | query: string().required(), 10 | variables: object(), 11 | }, 12 | handler: ({ query, variables }) => { 13 | return client 14 | .query({ query, variables, fetchPolicy: 'network-only' }) 15 | .then(({ data }) => data); 16 | }, 17 | }, 18 | mutate: { 19 | payload: { 20 | mutation: string().required(), 21 | variables: object(), 22 | }, 23 | handler: ({ mutation, variables }) => { 24 | return client.mutate({ mutation, variables }).then(({ data }) => data); 25 | }, 26 | }, 27 | }, 28 | }); 29 | 30 | export default apolloPersistor; 31 | -------------------------------------------------------------------------------- /packages/client/src/utils/assets.ts: -------------------------------------------------------------------------------- 1 | import config from '../config'; 2 | 3 | export const loadAsset = (localPath: string) => { 4 | return `${config.assetPath}/${localPath}`; 5 | }; 6 | -------------------------------------------------------------------------------- /packages/client/src/utils/authScope.ts: -------------------------------------------------------------------------------- 1 | import localPersistor from './localPersistor'; 2 | import { Scope } from 'lumbridge'; 3 | 4 | export const saveLocalAuth = localPersistor.instance({ 5 | name: 'store', 6 | map: ({ ...args }) => ({ 7 | ...args, 8 | id: 'auth', 9 | }), 10 | }); 11 | 12 | export const retrieveLocalAuth = localPersistor.instance({ 13 | name: 'retrieve', 14 | map: ({ ...args }) => ({ 15 | ...args, 16 | id: 'auth', 17 | }), 18 | }); 19 | 20 | const authScope: Scope = Scope.create(); 21 | 22 | authScope.absorb(saveLocalAuth); 23 | authScope.absorb(retrieveLocalAuth); 24 | 25 | export default authScope; 26 | -------------------------------------------------------------------------------- /packages/client/src/utils/authStore.ts: -------------------------------------------------------------------------------- 1 | import { Store } from 'lumbridge'; 2 | import * as Yup from 'yup'; 3 | 4 | const authStore: Store = Store.create({ 5 | schema: { 6 | token: { 7 | state: null, 8 | validate: Yup.string(), 9 | }, 10 | userId: { 11 | state: null, 12 | validate: Yup.string(), 13 | }, 14 | loggedIn: { 15 | state: false, 16 | validate: Yup.boolean().required(), 17 | }, 18 | }, 19 | actions: { 20 | patch: ({ token = null, userId = null } = {}) => ({ 21 | token, 22 | userId, 23 | loggedIn: Boolean(token && userId), 24 | }), 25 | }, 26 | }); 27 | 28 | export default authStore; 29 | -------------------------------------------------------------------------------- /packages/client/src/utils/components.ts: -------------------------------------------------------------------------------- 1 | export interface IComponentProps { 2 | handlers?: { 3 | [name: string]: (...args: any[]) => any; 4 | }; 5 | data?: { 6 | [name: string]: object | boolean | string | number | null | undefined; 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /packages/client/src/utils/electron.ts: -------------------------------------------------------------------------------- 1 | export const runElectron = ( 2 | withElectron: (electron: any) => void, 3 | withoutElectron?: () => void 4 | ) => { 5 | let electron; 6 | try { 7 | electron = (window as any).require('electron'); 8 | } catch (error) { 9 | electron = null; 10 | } 11 | if (electron) { 12 | return withElectron && withElectron(electron); 13 | } 14 | return withoutElectron && withoutElectron(); 15 | }; 16 | -------------------------------------------------------------------------------- /packages/client/src/utils/form.ts: -------------------------------------------------------------------------------- 1 | export interface IPrefill { 2 | [name: string]: any; 3 | } 4 | 5 | export const cleanFormPrefill = (defaults: IPrefill, values?: IPrefill) => { 6 | const filledValues = values || {}; 7 | const allKeys = { 8 | ...defaults, 9 | ...filledValues, 10 | }; 11 | return Object.keys(allKeys).reduce((accum, next) => { 12 | return { 13 | ...accum, 14 | [next]: filledValues[next] || defaults[next], 15 | }; 16 | }, {}); 17 | }; 18 | -------------------------------------------------------------------------------- /packages/client/src/utils/intercom.ts: -------------------------------------------------------------------------------- 1 | import config from '../config'; 2 | 3 | const defaults = { 4 | app_id: config.intercom, 5 | hide_default_launcher: true, 6 | custom_launcher_selector: '#intercom-launcher', 7 | }; 8 | 9 | export default { 10 | start(data: any = {}) { 11 | try { 12 | if ((window as any).Intercom && config.intercom) { 13 | (window as any).Intercom('boot', { ...defaults, ...data }); 14 | } 15 | } catch (e) { 16 | // code ... 17 | } 18 | }, 19 | 20 | update(data: any = {}) { 21 | try { 22 | if ((window as any).Intercom && config.intercom) { 23 | (window as any).Intercom('update', data); 24 | } 25 | } catch (e) { 26 | // code ... 27 | } 28 | }, 29 | 30 | shutdown() { 31 | try { 32 | if ((window as any).Intercom && config.intercom) { 33 | (window as any).Intercom('shutdown'); 34 | } 35 | } catch (e) { 36 | // code ... 37 | } 38 | }, 39 | 40 | restart(data: any = {}) { 41 | try { 42 | if ((window as any).Intercom && config.intercom) { 43 | (window as any).Intercom('shutdown'); 44 | (window as any).Intercom('boot', { ...defaults, ...data }); 45 | } 46 | } catch (e) { 47 | // code ... 48 | } 49 | }, 50 | }; 51 | -------------------------------------------------------------------------------- /packages/client/src/utils/localPersistor.ts: -------------------------------------------------------------------------------- 1 | import { Persistor } from 'lumbridge'; 2 | import * as Yup from 'yup'; 3 | 4 | const Store = (window as any).require('electron-store'); 5 | const store = new Store(); 6 | 7 | const localPersistor: Persistor = Persistor.create({ 8 | methods: { 9 | store: { 10 | payload: { 11 | id: Yup.string().required(), 12 | data: Yup.object().required(), 13 | }, 14 | handler: ({ id, data }) => { 15 | // @ts-ignore 16 | return new Promise((resolve, reject) => { 17 | try { 18 | const save = JSON.stringify(data); 19 | store.set(id, save); 20 | resolve(data || {}); 21 | } catch (error) { 22 | reject(error); 23 | } 24 | }); 25 | }, 26 | }, 27 | retrieve: { 28 | payload: { 29 | id: Yup.string().required(), 30 | }, 31 | handler: ({ id }) => { 32 | // @ts-ignore 33 | return new Promise((resolve, reject) => { 34 | try { 35 | const encode = store.get(id); 36 | const data = encode && JSON.parse(encode); 37 | resolve(data || {}); 38 | } catch (error) { 39 | store.delete(id); 40 | reject(error); 41 | } 42 | }); 43 | }, 44 | }, 45 | }, 46 | }); 47 | 48 | export default localPersistor; 49 | -------------------------------------------------------------------------------- /packages/client/src/utils/record.ts: -------------------------------------------------------------------------------- 1 | import Analytics from 'analytics-node'; 2 | import * as Sentry from '@sentry/browser'; 3 | import { titleCase } from 'change-case'; 4 | import config from '../config'; 5 | import authStore from './authStore'; 6 | 7 | const analytics = new Analytics(config.segmentId); 8 | 9 | /** 10 | * Make sure to catch errors so that the app keeps functioning. 11 | */ 12 | const captureAndEnsure = (cb: any) => (...args: any[]) => { 13 | if (config.segmentId) { 14 | try { 15 | cb(...args); 16 | } catch (error) { 17 | Sentry.captureException(error); 18 | } 19 | } 20 | }; 21 | 22 | /** 23 | * Recording changes of screen. 24 | */ 25 | export const recordPage = captureAndEnsure((data = {}) => { 26 | const userId = authStore.state.userId; 27 | const options = userId 28 | ? { userId } 29 | : { 30 | anonymousId: Math.random() 31 | .toString(36) 32 | .substr(3), 33 | }; 34 | analytics.page({ ...options, ...data }); 35 | }); 36 | 37 | /** 38 | * Identify a person with traits. 39 | */ 40 | export const recordUser = captureAndEnsure( 41 | ({ userId, traits }: { userId: string; traits: any }) => { 42 | const id = userId || authStore.state.userId; 43 | analytics.identify({ 44 | userId: id, 45 | traits, 46 | }); 47 | } 48 | ); 49 | 50 | /** 51 | * Record events which are specific to the front-end e.g. keyboard shortcuts etc. 52 | */ 53 | export const recordAction = captureAndEnsure( 54 | ({ 55 | userId, 56 | scope, 57 | action, 58 | properties = {}, 59 | }: { 60 | userId: string; 61 | scope: string; 62 | action: string; 63 | properties: any; 64 | }) => { 65 | const id = userId || authStore.state.userId; 66 | analytics.track({ 67 | userId: id, 68 | event: titleCase(`${scope} ${action}`), 69 | properties, 70 | }); 71 | } 72 | ); 73 | -------------------------------------------------------------------------------- /packages/client/src/utils/toastStore.ts: -------------------------------------------------------------------------------- 1 | import { Store } from 'lumbridge'; 2 | import * as Yup from 'yup'; 3 | 4 | const toastStore: Store = Store.create({ 5 | schema: { 6 | type: { 7 | state: null, 8 | validate: Yup.string(), 9 | }, 10 | contents: { 11 | state: null, 12 | validate: Yup.string(), 13 | }, 14 | }, 15 | actions: { 16 | ping: ({ type = null, contents = null } = {}) => ({ 17 | type, 18 | contents, 19 | }), 20 | }, 21 | }); 22 | 23 | export default toastStore; 24 | -------------------------------------------------------------------------------- /packages/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "esModuleInterop": true, 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "preserve" 16 | }, 17 | "include": [ 18 | "src" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /packages/server/.env.example: -------------------------------------------------------------------------------- 1 | AUTH_GITHUB_ID= 2 | AUTH_GITHUB_SECRET= 3 | MONGODB_URI= 4 | MONOGDB_USER_NAME= 5 | MONOGDB_USER_PASSWORD= 6 | SUPER_SECRET_TOKEN= 7 | STRIPE_SECRET_KEY= 8 | STRIPE_PLAN_STANDARD= 9 | SENTRY_DSN= 10 | SEGMENT_KEY= 11 | INTERCOM_SECRET= 12 | -------------------------------------------------------------------------------- /packages/server/.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "7" 4 | - "8" 5 | - "9" 6 | services: 7 | - mongodb 8 | notifications: 9 | email: 10 | on_success: change # default: change 11 | on_failure: change # default: always -------------------------------------------------------------------------------- /packages/server/README.md: -------------------------------------------------------------------------------- 1 | # Forge Server 2 | 3 | > 🏹 Unobtrusive and beautiful desktop app built to improve the development experience. 4 | 5 | This is the server code for the Forge app. -------------------------------------------------------------------------------- /packages/server/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "exec": "yarn start", 4 | "ext": "ts,tsx,gql", 5 | "ignore": [ 6 | "**/*.test.*", 7 | "**/*.spec.*" 8 | ] 9 | } -------------------------------------------------------------------------------- /packages/server/now.production.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "name": "forge-api-production", 4 | "dotenv": ".env.production", 5 | "env": { 6 | "NODE_ENV": "production" 7 | }, 8 | "alias": [ 9 | "api.v1.useforge.co" 10 | ], 11 | "scale": { 12 | "sfo1": { 13 | "min": 2, 14 | "max": 3 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "0.3.2", 4 | "private": true, 5 | "description": "Unobtrusive and beautiful desktop app built to improve the development experience.", 6 | "main": "src/index.js", 7 | "author": { 8 | "name": "Jack R. Scott", 9 | "email": "jack.rob.scott@gmail.com" 10 | }, 11 | "scripts": { 12 | "dev": "nodemon", 13 | "debug": "node --inspect=5858 -r ts-node/register src/index.ts", 14 | "start": "ts-node src/index.ts", 15 | "test": "NODE_ENV=test mocha --recursive --exit --require=ts-node/register 'src/**/*.test.ts'", 16 | "deploy": "now --local-config=now.production.json && now alias --local-config=now.production.json", 17 | "lint": "prettier --write --loglevel=warn 'src/**/*.ts' && tslint 'src/**/*.ts'" 18 | }, 19 | "dependencies": { 20 | "@sentry/node": "^4.4.1", 21 | "@types/analytics-node": "^0.0.33", 22 | "@types/bcryptjs": "^2.4.2", 23 | "@types/dotenv": "^6.1.0", 24 | "@types/dotenv-safe": "^5.0.2", 25 | "@types/escape-string-regexp": "^1.0.0", 26 | "@types/graphql": "^14.0.3", 27 | "@types/jsonwebtoken": "^8.3.0", 28 | "@types/lodash": "^4.14.118", 29 | "@types/mongoose": "^5.3.4", 30 | "@types/query-string": "^6.1.1", 31 | "@types/stripe": "^6.0.4", 32 | "analytics-node": "^3.3.0", 33 | "apollo-server": "^2.2.6", 34 | "axios": "^0.18.0", 35 | "bcryptjs": "^2.4.3", 36 | "dotenv": "^6.2.0", 37 | "dotenv-safe": "^6.1.0", 38 | "escape-string-regexp": "^1.0.5", 39 | "graphql": "^14.0.2", 40 | "graphql-tools": "^4.0.3", 41 | "jsonwebtoken": "^8.4.0", 42 | "lodash": "^4.17.11", 43 | "mongoose": "^5.3.15", 44 | "query-string": "^6.2.0", 45 | "stripe": "^6.18.1", 46 | "ts-node": "^7.0.1", 47 | "typescript": "^3.1.6" 48 | }, 49 | "devDependencies": { 50 | "@types/chai": "^4.1.7", 51 | "@types/mocha": "^5.2.5", 52 | "@types/sinon": "^5.0.7", 53 | "chai": "^4.2.0", 54 | "mocha": "^5.2.0", 55 | "nodemon": "^1.18.6", 56 | "sinon": "^7.1.1" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /packages/server/src/config.ts: -------------------------------------------------------------------------------- 1 | import * as dotenvSafe from 'dotenv-safe'; 2 | import * as dotenv from 'dotenv'; 3 | 4 | dotenvSafe.config({ allowEmptyValues: true }); 5 | dotenv.config(); 6 | 7 | interface IAuth { 8 | id: string; 9 | secret: string; 10 | url: string; 11 | api: string; 12 | } 13 | 14 | interface IAppConfig { 15 | environment: string; 16 | production: boolean; 17 | maxFreeCodes: number; 18 | mongodb: { 19 | uri: string; 20 | }; 21 | stripe: { 22 | key: string; 23 | plan: string; 24 | }; 25 | token: { 26 | secret: string; 27 | }; 28 | sentry: { 29 | dsn: string; 30 | }; 31 | segment: { 32 | key: string; 33 | }; 34 | intercom: { 35 | secret: string; 36 | }; 37 | auth: { 38 | github: IAuth; 39 | }; 40 | } 41 | 42 | const config: IAppConfig = { 43 | environment: process.env.NODE_ENV || 'development', 44 | production: process.env.NODE_ENV === 'production', 45 | maxFreeCodes: 10, 46 | mongodb: { 47 | uri: process.env.MONGODB_URI as string, 48 | }, 49 | stripe: { 50 | key: process.env.STRIPE_SECRET_KEY as string, 51 | plan: process.env.STRIPE_PLAN_STANDARD as string, 52 | }, 53 | token: { 54 | secret: process.env.SUPER_SECRET_TOKEN as string, 55 | }, 56 | sentry: { 57 | dsn: process.env.SENTRY_DSN as string, 58 | }, 59 | segment: { 60 | key: process.env.SEGMENT_KEY as string, 61 | }, 62 | intercom: { 63 | secret: process.env.INTERCOM_SECRET as string, 64 | }, 65 | auth: { 66 | github: { 67 | id: process.env.AUTH_GITHUB_ID as string, 68 | secret: process.env.AUTH_GITHUB_SECRET as string, 69 | url: 'https://github.com', 70 | api: 'https://api.github.com', 71 | }, 72 | }, 73 | }; 74 | 75 | export default config; 76 | -------------------------------------------------------------------------------- /packages/server/src/directives/AuthDirective.ts: -------------------------------------------------------------------------------- 1 | import { SchemaDirectiveVisitor } from 'graphql-tools'; 2 | import { 3 | defaultFieldResolver, 4 | GraphQLObjectType, 5 | GraphQLInterfaceType, 6 | } from 'graphql'; 7 | import { AuthenticationError } from 'apollo-server'; 8 | 9 | export class AuthDirective extends SchemaDirectiveVisitor { 10 | public visitObject(object: GraphQLObjectType) { 11 | this.ensureFieldsWrapped(object); 12 | } 13 | 14 | public visitFieldDefinition( 15 | _: any, 16 | details: { 17 | objectType: GraphQLObjectType | GraphQLInterfaceType; 18 | } 19 | ) { 20 | this.ensureFieldsWrapped(details.objectType); 21 | } 22 | 23 | protected ensureFieldsWrapped( 24 | objectType: (GraphQLObjectType | GraphQLInterfaceType) & { 25 | _wrappedAuth?: boolean; 26 | } 27 | ) { 28 | if (objectType._wrappedAuth) { 29 | return; 30 | } else { 31 | objectType._wrappedAuth = true; 32 | } 33 | const fields = objectType.getFields(); 34 | Object.keys(fields).forEach(fieldName => { 35 | const field = fields[fieldName]; 36 | const { resolve = defaultFieldResolver } = field; 37 | field.resolve = async function(...args) { 38 | const { user } = args[2]; 39 | if (!user) { 40 | throw new AuthenticationError('Access denied.'); 41 | } 42 | return resolve.apply(this, args); 43 | }; 44 | }); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/server/src/index.ts: -------------------------------------------------------------------------------- 1 | import { ApolloServer } from 'apollo-server'; 2 | import { makeExecutableSchema } from 'graphql-tools'; 3 | import { merge } from 'lodash'; 4 | import { connect, connection } from 'mongoose'; 5 | import config from './config'; 6 | import { decode } from './utils/auth'; 7 | import { captureRequestData, capture } from './utils/errors'; 8 | import User from './models/User'; 9 | import { AuthDirective } from './directives/AuthDirective'; 10 | import bundleResolvers from './resolvers/bundleResolvers'; 11 | import codeResolvers from './resolvers/codeResolvers'; 12 | import providerResolvers from './resolvers/providerResolvers'; 13 | import userResolvers from './resolvers/userResolvers'; 14 | import optinResolvers from './resolvers/optinResolvers'; 15 | import { readFileSync, readdirSync } from 'fs'; 16 | import { join } from 'path'; 17 | 18 | /** 19 | * Connect to the mongodb database using 20 | * the mongoose library. 21 | */ 22 | connect( 23 | config.mongodb.uri, 24 | { useNewUrlParser: true } 25 | ); 26 | connection.on('error', error => { 27 | throw error; 28 | }); 29 | 30 | /** 31 | * Load all gql files into an array of strings. 32 | */ 33 | const typeDefs = readdirSync(join(__dirname, './schemas/')).map(file => { 34 | return readFileSync(join(__dirname, './schemas/', file)).toString('utf-8'); 35 | }); 36 | 37 | /** 38 | * Declare the schema which the will hold our 39 | * GraphQL types and resolvers. 40 | */ 41 | const schema = makeExecutableSchema({ 42 | typeDefs, 43 | resolvers: merge( 44 | bundleResolvers, 45 | codeResolvers, 46 | providerResolvers, 47 | userResolvers, 48 | optinResolvers 49 | ) as any, 50 | schemaDirectives: { 51 | auth: AuthDirective, 52 | }, 53 | resolverValidationOptions: { 54 | requireResolversForResolveType: false, 55 | }, 56 | }); 57 | 58 | /** 59 | * Create the server which we will send our 60 | * GraphQL queries to. 61 | */ 62 | const server = new ApolloServer({ 63 | schema, 64 | formatError: capture, 65 | async context({ req }: any) { 66 | captureRequestData({ req }); 67 | const token = req && req.headers && req.headers.authorization; 68 | if (token) { 69 | const data = decode(token) as { userId: string }; 70 | const user = data.userId ? await User.findById(data.userId) : null; 71 | return { user }; 72 | } 73 | }, 74 | }); 75 | 76 | /** 77 | * Turn the server on by listening to a port, 78 | * defaults to: http://localhost:4000 79 | */ 80 | server.listen().then(({ url }) => { 81 | console.log(`🚀 Server ready at ${url}`); 82 | }); 83 | -------------------------------------------------------------------------------- /packages/server/src/models/Bundle.ts: -------------------------------------------------------------------------------- 1 | import { Schema, Document, model } from 'mongoose'; 2 | import { modelOptions } from '../utils/models'; 3 | 4 | export interface IBundle extends Document { 5 | id: string; 6 | createdAt: string; 7 | updatedAt: string; 8 | creatorId: Schema.Types.ObjectId | string; 9 | name: string; 10 | readme: string; 11 | published: boolean; 12 | toRecord: (extra?: object) => object; 13 | } 14 | 15 | const schema = { 16 | creatorId: { 17 | type: Schema.Types.ObjectId, 18 | ref: 'User', 19 | required: true, 20 | }, 21 | name: { 22 | type: String, 23 | required: true, 24 | }, 25 | readme: { 26 | type: String, 27 | required: true, 28 | }, 29 | published: { 30 | type: Boolean, 31 | required: true, 32 | default: false, 33 | }, 34 | }; 35 | 36 | const bundleSchema: Schema = new Schema(schema, modelOptions); 37 | 38 | bundleSchema.method('toRecord', function toRecord(this: any, extra: object) { 39 | const { id, createdAt, updatedAt, creatorId, name, readme } = this.toObject(); 40 | return { 41 | id, 42 | createdAt, 43 | updatedAt, 44 | creatorId, 45 | name, 46 | readme, 47 | ...(extra || {}), 48 | }; 49 | }); 50 | 51 | export default model('Bundle', bundleSchema); 52 | -------------------------------------------------------------------------------- /packages/server/src/models/Code.ts: -------------------------------------------------------------------------------- 1 | import { Schema, Document, model } from 'mongoose'; 2 | import { modelOptions } from '../utils/models'; 3 | 4 | export interface ICode extends Document { 5 | id: string; 6 | createdAt: string; 7 | updatedAt: string; 8 | creatorId: Schema.Types.ObjectId | string; 9 | bundleId?: Schema.Types.ObjectId | string; 10 | name: string; 11 | shortcut: string; 12 | contents: string; 13 | language?: string; 14 | toRecord: (extra?: object) => object; 15 | } 16 | 17 | const schema = { 18 | creatorId: { 19 | type: Schema.Types.ObjectId, 20 | ref: 'User', 21 | required: true, 22 | }, 23 | bundleId: { 24 | type: Schema.Types.ObjectId, 25 | ref: 'Bundle', 26 | }, 27 | name: { 28 | type: String, 29 | required: true, 30 | }, 31 | shortcut: { 32 | type: String, 33 | required: true, 34 | }, 35 | contents: { 36 | type: String, 37 | required: true, 38 | }, 39 | language: { 40 | type: String, 41 | }, 42 | }; 43 | 44 | const codeSchema = new Schema(schema, modelOptions); 45 | 46 | codeSchema.method('toRecord', function toRecord(this: any, extra: object) { 47 | const { 48 | id, 49 | createdAt, 50 | updatedAt, 51 | creatorId, 52 | name, 53 | shortcut, 54 | } = this.toObject(); 55 | return { 56 | id, 57 | createdAt, 58 | updatedAt, 59 | creatorId, 60 | name, 61 | shortcut, 62 | ...(extra || {}), 63 | }; 64 | }); 65 | 66 | export default model('Code', codeSchema); 67 | -------------------------------------------------------------------------------- /packages/server/src/models/Optin.ts: -------------------------------------------------------------------------------- 1 | import { Schema, Document, model } from 'mongoose'; 2 | import { modelOptions } from '../utils/models'; 3 | 4 | export interface IOptin extends Document { 5 | id: string; 6 | createdAt: string; 7 | updatedAt: string; 8 | userId: Schema.Types.ObjectId | string; 9 | bundleId: Schema.Types.ObjectId | string; 10 | toRecord: (extra?: object) => object; 11 | } 12 | 13 | const schema = { 14 | bundleId: { 15 | type: Schema.Types.ObjectId, 16 | ref: 'Bundle', 17 | required: true, 18 | }, 19 | userId: { 20 | type: Schema.Types.ObjectId, 21 | ref: 'User', 22 | required: true, 23 | }, 24 | }; 25 | 26 | const optinSchema = new Schema(schema, modelOptions); 27 | 28 | optinSchema.method('toRecord', function toRecord(this: any, extra: object) { 29 | const { id, createdAt, updatedAt, userId, bundleId } = this.toObject(); 30 | return { 31 | id, 32 | createdAt, 33 | updatedAt, 34 | userId, 35 | bundleId, 36 | ...(extra || {}), 37 | }; 38 | }); 39 | 40 | export default model('Optin', optinSchema); 41 | -------------------------------------------------------------------------------- /packages/server/src/models/Provider.ts: -------------------------------------------------------------------------------- 1 | import { Schema, Document, model } from 'mongoose'; 2 | import { modelOptions } from '../utils/models'; 3 | 4 | export interface IProvider extends Document { 5 | id: string; 6 | createdAt: string; 7 | updatedAt: string; 8 | creatorId: Schema.Types.ObjectId | string; 9 | domain: string; 10 | payload: { 11 | [name: string]: string | boolean | number; 12 | }; 13 | toRecord: (extra?: object) => object; 14 | } 15 | 16 | const schema = { 17 | creatorId: { 18 | type: Schema.Types.ObjectId, 19 | ref: 'User', 20 | required: true, 21 | }, 22 | domain: { 23 | type: String, 24 | enum: ['github', 'custom'], 25 | required: true, 26 | }, 27 | payload: { 28 | type: Schema.Types.Mixed, 29 | required: true, 30 | }, 31 | }; 32 | 33 | const providerSchema = new Schema(schema, modelOptions); 34 | 35 | providerSchema.method('toRecord', function toRecord(this: any, extra: object) { 36 | const { id, createdAt, updatedAt, creatorId } = this.toObject(); 37 | return { 38 | id, 39 | createdAt, 40 | updatedAt, 41 | creatorId, 42 | ...(extra || {}), 43 | }; 44 | }); 45 | 46 | export default model('Provider', providerSchema); 47 | -------------------------------------------------------------------------------- /packages/server/src/models/User.ts: -------------------------------------------------------------------------------- 1 | import { Schema, Document, model } from 'mongoose'; 2 | import { modelOptions } from '../utils/models'; 3 | 4 | export interface IUser extends Document { 5 | id: string; 6 | createdAt: string; 7 | updatedAt: string; 8 | email: string; 9 | name: string; 10 | avatar?: string; 11 | customer: { 12 | id: string; 13 | }; 14 | subscription: { 15 | id: string; 16 | prevId: string; 17 | }; 18 | preferences: { 19 | shortcutOpen: string; 20 | searchOnOpen: boolean; 21 | }; 22 | toRecord: (extra?: object) => object; 23 | } 24 | 25 | const schema = { 26 | username: { 27 | type: String, 28 | required: true, 29 | unique: true, 30 | validate: { 31 | validator: (value: any) => /^[0-9a-z\-]+$/.test(value), 32 | message: ({ value }: any) => 33 | `${value} must only be letters, numbers and dashes e.g. "one-two-three".`, 34 | }, 35 | }, 36 | email: { 37 | type: String, 38 | required: true, 39 | }, 40 | name: { 41 | type: String, 42 | }, 43 | avatar: { 44 | type: String, 45 | }, 46 | customer: { 47 | id: { 48 | type: String, 49 | }, 50 | }, 51 | subscription: { 52 | id: { 53 | type: String, 54 | }, 55 | prevId: { 56 | type: String, 57 | }, 58 | }, 59 | preferences: { 60 | shortcutOpen: { 61 | type: String, 62 | }, 63 | searchOnOpen: { 64 | type: Boolean, 65 | }, 66 | }, 67 | }; 68 | 69 | const userSchema = new Schema(schema, modelOptions); 70 | 71 | /** 72 | * Is this customer paying? 73 | */ 74 | userSchema.virtual('isSubscribed').get(function(this: any) { 75 | return Boolean( 76 | this.customer && 77 | this.customer.id && 78 | this.customer.id.length && 79 | this.subscription && 80 | this.subscription.id && 81 | this.subscription.id.length 82 | ); 83 | }); 84 | 85 | userSchema.method('toRecord', function toRecord(this: any, extra: object) { 86 | const { 87 | id, 88 | createdAt, 89 | updatedAt, 90 | email, 91 | name, 92 | customer, 93 | subscription, 94 | } = this.toObject(); 95 | return { 96 | id, 97 | createdAt, 98 | updatedAt, 99 | email, 100 | name, 101 | customerId: customer && customer.id, 102 | subscriptionId: subscription && subscription.id, 103 | ...(extra || {}), 104 | }; 105 | }); 106 | 107 | export default model('User', userSchema); 108 | -------------------------------------------------------------------------------- /packages/server/src/resolvers/bundleResolvers.ts: -------------------------------------------------------------------------------- 1 | import Bundle from '../models/Bundle'; 2 | import User from '../models/User'; 3 | import { recordAction } from '../utils/record'; 4 | 5 | export default { 6 | Query: { 7 | async userBundles( 8 | _: any, 9 | { filter, search }: { filter?: object; search?: string }, 10 | { user }: { user: any } 11 | ) { 12 | const options: any = { creatorId: user.id }; 13 | if (search && search.length) { 14 | const regSearch = new RegExp(search, 'i'); 15 | options.$or = [{ name: { $regex: regSearch } }]; 16 | } 17 | const bundles: any[] = await Bundle.find(options, null, filter); 18 | return bundles.map(bundle => bundle.toObject()); 19 | }, 20 | async marketBundles( 21 | _: any, 22 | { filter, search }: { filter?: object; search?: string } 23 | ) { 24 | const options: any = { 25 | published: true, 26 | }; 27 | if (search && search.length) { 28 | const regSearch = new RegExp(search, 'i'); 29 | options.$or = [{ name: { $regex: regSearch } }]; 30 | } 31 | const bundles: any[] = await Bundle.find(options, null, filter); 32 | return bundles.map(bundle => bundle.toObject()); 33 | }, 34 | async bundle(_: any, { id }: { id: string }) { 35 | const bundle: any = await Bundle.findById(id); 36 | return bundle.toObject(); 37 | }, 38 | }, 39 | Mutation: { 40 | async addBundle( 41 | _: any, 42 | { input }: { input: object }, 43 | { user }: { user: any } 44 | ) { 45 | const bundle: any = await Bundle.create({ 46 | ...input, 47 | creatorId: user && user.id, 48 | }); 49 | recordAction({ 50 | userId: bundle.creatorId, 51 | scope: 'Bundle', 52 | action: 'Created', 53 | properties: bundle.toRecord(), 54 | }); 55 | return bundle.toObject(); 56 | }, 57 | async editBundle(_: any, { id, input }: { id: string; input: object }) { 58 | const bundle: any = await Bundle.findById(id); 59 | Object.assign(bundle, input); 60 | await bundle.save(); 61 | recordAction({ 62 | userId: bundle.creatorId, 63 | scope: 'Bundle', 64 | action: 'Updated', 65 | properties: bundle.toRecord(), 66 | }); 67 | return bundle.toObject(); 68 | }, 69 | async deleteBundle(_: any, { id }: { id: string }) { 70 | const bundle: any = await Bundle.findById(id); 71 | await bundle.remove(); 72 | recordAction({ 73 | userId: bundle.creatorId, 74 | scope: 'Bundle', 75 | action: 'Removed', 76 | properties: bundle.toRecord(), 77 | }); 78 | return bundle ? bundle.toObject() : null; 79 | }, 80 | }, 81 | Bundle: { 82 | async creator({ creatorId }: { creatorId?: string }) { 83 | if ({ creatorId }) { 84 | const user: any = await User.findById(creatorId); 85 | return user.toObject(); 86 | } 87 | return null; 88 | }, 89 | }, 90 | }; 91 | -------------------------------------------------------------------------------- /packages/server/src/resolvers/optinResolvers.ts: -------------------------------------------------------------------------------- 1 | import Bundle from '../models/Bundle'; 2 | import User from '../models/User'; 3 | import Optin from '../models/Optin'; 4 | 5 | export default { 6 | Mutation: { 7 | async optinUser( 8 | _: any, 9 | { bundleId }: { bundleId: string }, 10 | { user }: { user: any } 11 | ) { 12 | if (!user.isSubscribed) { 13 | throw new Error( 14 | `You can not add a bundle to your library while in demo mode.\n\nHave to pay the bills some how...\n\n¯\\_(ツ)_/¯` 15 | ); 16 | } 17 | const optin: any = await Optin.create({ bundleId, userId: user.id }); 18 | return optin.toObject(); 19 | }, 20 | async optoutUser( 21 | _: any, 22 | { bundleId }: { bundleId: string }, 23 | { user }: { user: any } 24 | ) { 25 | const optin: any = await Optin.findOne({ bundleId, userId: user.id }); 26 | await optin.remove(); 27 | return optin.toObject ? optin.toObject() : null; 28 | }, 29 | }, 30 | Optin: { 31 | async user({ userId }: { userId: string }) { 32 | const user: any = await User.findById(userId); 33 | return user.toObject(); 34 | }, 35 | async bundle({ bundleId }: { bundleId: string }) { 36 | const bundle: any = await Bundle.findById(bundleId); 37 | return bundle.toObject(); 38 | }, 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /packages/server/src/schemas/@auth.gql: -------------------------------------------------------------------------------- 1 | directive @auth on OBJECT | FIELD_DEFINITION -------------------------------------------------------------------------------- /packages/server/src/schemas/Bundle.gql: -------------------------------------------------------------------------------- 1 | type Bundle @auth { 2 | id: ID! 3 | createdAt: Date! 4 | updatedAt: Date! 5 | creatorId: String! 6 | name: String! 7 | readme: String! 8 | published: Boolean! 9 | # Extras 10 | creator: User 11 | } 12 | 13 | input BundleFilterInput { 14 | limit: Int 15 | } 16 | 17 | input BundleInput { 18 | name: String 19 | readme: String 20 | published: Boolean 21 | } 22 | 23 | extend type Query { 24 | userBundles(search: String, filter: BundleFilterInput): [Bundle!]! 25 | marketBundles(search: String, filter: BundleFilterInput): [Bundle!]! 26 | bundle(id: String!): Bundle! 27 | } 28 | 29 | extend type Mutation { 30 | addBundle(input: BundleInput!): Bundle 31 | editBundle(id: String!, input: BundleInput!): Bundle 32 | deleteBundle(id: String!): Bundle 33 | } -------------------------------------------------------------------------------- /packages/server/src/schemas/Code.gql: -------------------------------------------------------------------------------- 1 | type Code @auth { 2 | id: ID! 3 | createdAt: Date! 4 | updatedAt: Date! 5 | creatorId: String! 6 | bundleId: String 7 | name: String! 8 | shortcut: String! 9 | contents: String! 10 | language: String 11 | # Extras 12 | creator: User 13 | bundle: Bundle 14 | } 15 | 16 | input CodeFilterInput { 17 | limit: Int 18 | } 19 | 20 | input CodeInput { 21 | name: String 22 | shortcut: String 23 | contents: String 24 | language: String 25 | } 26 | 27 | extend type Query { 28 | userCodes(search: String, filter: CodeFilterInput): [Code!]! 29 | codes(filter: CodeFilterInput): [Code!]! 30 | code(id: String!): Code! 31 | } 32 | 33 | extend type Mutation { 34 | addCode(input: CodeInput!): Code 35 | editCode(id: String!, input: CodeInput!): Code 36 | cloneCode(id: String!): Code 37 | deleteCode(id: String!): Code 38 | } -------------------------------------------------------------------------------- /packages/server/src/schemas/Optin.gql: -------------------------------------------------------------------------------- 1 | type Optin { 2 | id: ID! 3 | createdAt: Date! 4 | updatedAt: Date! 5 | userId: String! 6 | bundleId: String! 7 | # Extras 8 | user: User 9 | bundle: Bundle 10 | } 11 | 12 | extend type Mutation { 13 | optinUser(bundleId: String!): Optin 14 | optoutUser(bundleId: String!): Optin 15 | } -------------------------------------------------------------------------------- /packages/server/src/schemas/Provider.gql: -------------------------------------------------------------------------------- 1 | interface Provider { 2 | id: ID! 3 | createdAt: Date! 4 | updatedAt: Date! 5 | creatorId: String! 6 | domain: String! 7 | # Extras 8 | creator: User 9 | } 10 | 11 | type GitHubPayload { 12 | userId: String! 13 | accessToken: String! 14 | } 15 | 16 | type GitHub implements Provider { 17 | id: ID! 18 | createdAt: Date! 19 | updatedAt: Date! 20 | creatorId: String! 21 | domain: String! 22 | payload: GitHubPayload! 23 | # Extras 24 | creator: User 25 | } 26 | 27 | type CustomPayload { 28 | username: String! 29 | password: String! 30 | } 31 | 32 | type Custom implements Provider { 33 | id: ID! 34 | createdAt: Date! 35 | updatedAt: Date! 36 | creatorId: String! 37 | domain: String! 38 | payload: CustomPayload! 39 | # Extras 40 | creator: User 41 | } 42 | 43 | type Payload { 44 | token: String! 45 | userId: String! 46 | } 47 | 48 | extend type Query { 49 | oauthGitHubUrl: String! 50 | userConnectedGitHub: Boolean! 51 | } 52 | 53 | extend type Mutation { 54 | authCreateCustom(username: String!, password: String!, email: String!): Payload! 55 | authLoginCustom(username: String!, password: String!): Payload! 56 | authPasswordChangeCustom(oldPassword: String!, newPassword: String!): Payload! 57 | authConnectGitHub(code: String!): Payload! 58 | authLoginGitHub(code: String!): Payload! 59 | } -------------------------------------------------------------------------------- /packages/server/src/schemas/User.gql: -------------------------------------------------------------------------------- 1 | type ChargeCustomer { 2 | id: String 3 | } 4 | 5 | type ChargeSubscription { 6 | id: String 7 | prevId: String 8 | } 9 | 10 | type Preferences { 11 | shortcutOpen: String 12 | searchOnOpen: Boolean 13 | } 14 | 15 | type User @auth { 16 | id: ID! 17 | createdAt: Date! 18 | updatedAt: Date! 19 | username: String! 20 | email: String! 21 | name: String 22 | avatar: String 23 | isSubscribed: Boolean! 24 | customer: ChargeCustomer 25 | subscription: ChargeSubscription 26 | preferences: Preferences 27 | # Extras 28 | optins: [Optin!]! 29 | hasOptin(bundleId: String!): Boolean! 30 | hash: String! 31 | } 32 | 33 | input UserFilterInput { 34 | limit: Int 35 | } 36 | 37 | input PreferencesInput { 38 | shortcutOpen: String 39 | searchOnOpen: Boolean 40 | } 41 | 42 | input UserInput { 43 | email: String 44 | name: String 45 | avatar: String 46 | preferences: PreferencesInput 47 | } 48 | 49 | extend type Query { 50 | users(filter: UserFilterInput): [User]! 51 | user(id: String!): User! 52 | me: User! 53 | } 54 | 55 | extend type Mutation { 56 | addUser(input: UserInput!): User 57 | editUser(id: String!, input: UserInput!): User 58 | editMe(input: UserInput!): User 59 | deleteUser(id: String!): User 60 | subscribeUser(token: String!, coupon: String): User 61 | unsubscribeUser: User 62 | } -------------------------------------------------------------------------------- /packages/server/src/schemas/schema.gql: -------------------------------------------------------------------------------- 1 | scalar Date 2 | 3 | type Query 4 | type Mutation 5 | 6 | schema { 7 | query: Query 8 | mutation: Mutation 9 | } -------------------------------------------------------------------------------- /packages/server/src/utils/auth.ts: -------------------------------------------------------------------------------- 1 | import * as jwt from 'jsonwebtoken'; 2 | import config from '../config'; 3 | 4 | export const encode = (data: any): string => { 5 | return jwt.sign(data, config.token.secret); 6 | }; 7 | 8 | export const decode = (token: string): string | object => { 9 | return jwt.verify(decodeURIComponent(token), config.token.secret); 10 | }; 11 | -------------------------------------------------------------------------------- /packages/server/src/utils/errors.ts: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/node'; 2 | import config from '../config'; 3 | import { GraphQLError } from 'graphql'; 4 | 5 | Sentry.init({ 6 | // dsn: config.production ? config.sentry.dsn : '', 7 | dsn: config.sentry.dsn, 8 | }); 9 | 10 | export const capture = (error: any) => { 11 | const { code, exception } = error.extensions; 12 | if ( 13 | code === 'INTERNAL_SERVER_ERROR' && 14 | exception.name !== 'ValidationError' 15 | ) { 16 | if (config.production) { 17 | Sentry.captureException(exception); 18 | } else { 19 | console.log(exception); 20 | } 21 | } 22 | return error; 23 | }; 24 | 25 | export const captureRequestData = ({ 26 | req, 27 | options, 28 | }: { 29 | req: any; 30 | options?: any; 31 | }) => { 32 | Sentry.configureScope(scope => { 33 | scope.addEventProcessor(async event => 34 | Sentry.Handlers.parseRequest(event, req, options) 35 | ); 36 | }); 37 | }; 38 | -------------------------------------------------------------------------------- /packages/server/src/utils/github.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import * as queryString from 'query-string'; 3 | import config from '../config'; 4 | 5 | export const resolveCode = async (code: string) => { 6 | const options = { 7 | client_id: config.auth.github.id, 8 | client_secret: config.auth.github.secret, 9 | code, 10 | }; 11 | const url = `${ 12 | config.auth.github.url 13 | }/login/oauth/access_token?${queryString.stringify(options)}`; 14 | const { data } = await axios.get(url); 15 | const { error, error_description, access_token } = queryString.parse(data); 16 | if (error) { 17 | throw new Error( 18 | error_description && error_description.length 19 | ? error_description[0] 20 | : 'The action could not be performed because of an unknown error.' 21 | ); 22 | } 23 | const ghUserResponse: { 24 | data: { 25 | id: string; 26 | email: string; 27 | name: string; 28 | login: string; 29 | avatar_url: string; 30 | }; 31 | } = await axios.get(`${config.auth.github.api}/user`, { 32 | headers: { 33 | Authorization: `token ${access_token}`, 34 | }, 35 | }); 36 | const ghUserData = ghUserResponse.data; 37 | if (!ghUserData.email) { 38 | const emailResponse = await axios.get( 39 | `${config.auth.github.api}/user/emails`, 40 | { 41 | headers: { 42 | Authorization: `token ${access_token}`, 43 | }, 44 | } 45 | ); 46 | const ghEmailData = emailResponse.data[0]; 47 | if (ghEmailData && ghEmailData.email) { 48 | ghUserData.email = ghEmailData.email; 49 | } 50 | } 51 | return { ghUserResponse, ghUserData, access_token }; 52 | }; 53 | -------------------------------------------------------------------------------- /packages/server/src/utils/models.ts: -------------------------------------------------------------------------------- 1 | export const modelOptions = { 2 | timestamps: true, 3 | toObject: { getters: true }, 4 | }; 5 | 6 | export const compareIds = (one: any, two: any) => String(one) === String(two); 7 | -------------------------------------------------------------------------------- /packages/server/src/utils/password.ts: -------------------------------------------------------------------------------- 1 | import * as bcrypt from 'bcryptjs'; 2 | 3 | export const hashPassword = async (password: string): Promise => { 4 | return bcrypt.genSalt(5).then(salt => bcrypt.hash(password, salt)); 5 | }; 6 | 7 | export const comparePassword = async ( 8 | candidate: string, 9 | password: string 10 | ): Promise => { 11 | return bcrypt.compare(candidate, password); 12 | }; 13 | -------------------------------------------------------------------------------- /packages/server/src/utils/payments.ts: -------------------------------------------------------------------------------- 1 | import * as Stripe from 'stripe'; 2 | import config from '../config'; 3 | 4 | const stripe = new Stripe(config.stripe.key); 5 | 6 | export const paymentsCustomerCreate = ({ 7 | email, 8 | source, 9 | ...options 10 | }: { 11 | email: string; 12 | source: string; 13 | [name: string]: any; 14 | }) => { 15 | return stripe.customers.create({ 16 | email, 17 | source, 18 | ...options, 19 | }); 20 | }; 21 | 22 | export const paymentsCustomerSubscribe = ( 23 | customerId: string, 24 | options: { 25 | [name: string]: any; 26 | } 27 | ) => { 28 | return stripe.subscriptions.create({ 29 | customer: customerId, 30 | items: [{ plan: config.stripe.plan }], 31 | ...options, 32 | }); 33 | }; 34 | 35 | export const paymentsCustomerUnsubscribe = (subscriptionId: string) => { 36 | return stripe.subscriptions.del(subscriptionId); 37 | }; 38 | -------------------------------------------------------------------------------- /packages/server/src/utils/record.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import * as Analytics from 'analytics-node'; 3 | import * as Sentry from '@sentry/node'; 4 | import config from '../config'; 5 | 6 | const analytics = new Analytics(config.segment.key); 7 | 8 | const captureErrors = (cb: (...args: any[]) => any) => (...args: any[]) => { 9 | try { 10 | cb(...args); 11 | } catch (error) { 12 | Sentry.captureException(error); 13 | } 14 | }; 15 | 16 | export const recordUser = captureErrors( 17 | ({ userId, traits }: { userId: string; traits: object }) => 18 | analytics.identify({ 19 | userId: String(userId), 20 | traits, 21 | }) 22 | ); 23 | 24 | export const recordAction = captureErrors( 25 | ({ 26 | userId, 27 | scope, 28 | action, 29 | properties, 30 | }: { 31 | userId: string; 32 | scope: string; 33 | action: string; 34 | properties?: object; 35 | }) => 36 | analytics.track({ 37 | userId: String(userId), 38 | event: `${scope} ${action}`, 39 | properties: properties || {}, 40 | }) 41 | ); 42 | -------------------------------------------------------------------------------- /packages/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */ 4 | "module": "commonjs", /* Specify module code generation: 'none', commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 5 | // "lib": [], /* Specify library files to be included in the compilation: */ 6 | // "allowJs": true, /* Allow javascript files to be compiled. */ 7 | // "checkJs": true, /* Report errors in .js files. */ 8 | "jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 9 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 10 | "sourceMap": true, /* Generates corresponding '.map' file. */ 11 | // "outFile": "./", /* Concatenate and emit output to single file. */ 12 | "outDir": "lib", /* Redirect output structure to the directory. */ 13 | "rootDir": "src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 14 | // "removeComments": true, /* Do not emit comments to output. */ 15 | // "noEmit": true, /* Do not emit outputs. */ 16 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 17 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 18 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 19 | "strict": true, /* Enable all strict type-checking options. */ 20 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 21 | // "strictNullChecks": true, /* Enable strict null checks. */ 22 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 23 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 24 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 25 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 26 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 27 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 28 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 29 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 30 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 31 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 32 | // "typeRoots": [], /* List of folders to include type definitions from. */ 33 | // "types": [], /* Type declaration files to be included in compilation. */ 34 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 35 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 36 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 37 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ 38 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 39 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 40 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 41 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 42 | }, 43 | "exclude": [ 44 | "src/**/*.test.*", 45 | "src/**/*.spec.*", 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:latest", 5 | "tslint-eslint-rules", 6 | "tslint-config-prettier", 7 | "tslint-react" 8 | ], 9 | "jsRules": {}, 10 | "rules": { 11 | "ordered-imports": false, 12 | "object-literal-sort-keys": false, 13 | "no-implicit-dependencies": [true, "dev"], 14 | "no-submodule-imports": false, 15 | "no-empty-interface": false, 16 | "no-console": false 17 | }, 18 | "rulesDirectory": [] 19 | } --------------------------------------------------------------------------------