├── .eslintrc ├── .github └── workflows │ └── typescript.yml ├── .gitignore ├── README.md ├── __.babelrc ├── components ├── .DS_Store ├── ability.tsx ├── about.tsx ├── actions.tsx ├── base-size.tsx ├── collection-panel.tsx ├── dial │ ├── index.tsx │ └── row.tsx ├── fonts │ ├── .DS_Store │ ├── ships.tsx │ └── xwing.tsx ├── format-error.tsx ├── format.tsx ├── formatted-text.tsx ├── grants.tsx ├── import.tsx ├── layout.tsx ├── limit-error.tsx ├── logo.tsx ├── modal.tsx ├── notification.tsx ├── pilot.tsx ├── popover │ ├── pilot.tsx │ ├── ship.tsx │ └── upgrade.tsx ├── save-panel.tsx ├── saved-squadrons-panel.tsx ├── search │ ├── index.tsx │ └── input.tsx ├── select-tags.tsx ├── ship-stats.tsx ├── ship-type.tsx ├── slim │ ├── pilot.tsx │ ├── ship.tsx │ └── upgrade.tsx ├── tag.tsx ├── unit.tsx └── upgrade.tsx ├── helpers ├── clipboard.ts ├── collection.ts ├── colors.ts ├── convert.ts ├── cost.ts ├── edit.ts ├── export.ts ├── images.ts ├── import.ts ├── loading.ts ├── misc.ts ├── names.ts ├── popover.ts ├── request.ts ├── select.ts └── types.ts ├── next-env.d.ts ├── next.config.js ├── nodemon.json ├── package.json ├── pages ├── _app.tsx ├── api │ ├── auth │ │ └── [...nextauth].ts │ ├── link.ts │ ├── standard.ts │ └── xws.ts ├── blog │ └── [id].tsx ├── index.tsx ├── plot.tsx ├── print.tsx └── privacy.tsx ├── postcss.config.js ├── public ├── .well-known │ └── apple-app-site-association ├── android-chrome-192x192.png ├── android-chrome-256x256.png ├── apple-touch-icon.png ├── browserconfig.xml ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── mstile-150x150.png ├── safari-pinned-tab.svg ├── site.webmanifest └── static │ ├── css │ └── tailwind.css │ ├── fonts │ ├── xwing-miniatures-ships.ttf │ └── xwing-miniatures.ttf │ ├── images │ ├── 1_1.png │ ├── 1_2.png │ ├── 1_3.png │ └── 1_4.png │ └── plot_data.json ├── scripts ├── apple-gen-secret.mjs └── post-install.js ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.server.json ├── tsconfig.tsbuildinfo ├── yarn-error.log └── yarn.lock /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:react/recommended" 5 | ], 6 | "parser": "babel-eslint", 7 | "plugins": [ 8 | "react", 9 | "prettier", 10 | "react-hooks" 11 | ], 12 | "parserOptions": { 13 | "ecmaFeatures": { 14 | "jsx": true 15 | }, 16 | "sourceType": "module" 17 | }, 18 | "rules": { 19 | "prettier/prettier": [ 20 | "error", 21 | { 22 | "trailingComma": "es5", 23 | "singleQuote": true 24 | } 25 | ], 26 | "react/display-name": 0, 27 | "no-unused-vars": 2, 28 | "react/jsx-uses-vars": [ 29 | "error" 30 | ], 31 | "react/sort-comp": 0, 32 | "react/jsx-filename-extension": [ 33 | 1, 34 | { 35 | "extensions": [ 36 | ".js", 37 | ".jsx" 38 | ] 39 | } 40 | ], 41 | "react/no-unused-prop-types": 0, 42 | "no-undef": 0, 43 | "react-hooks/rules-of-hooks": "error", 44 | "react-hooks/exhaustive-deps": "warn" 45 | } 46 | } -------------------------------------------------------------------------------- /.github/workflows/typescript.yml: -------------------------------------------------------------------------------- 1 | name: Typescript 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [8.x, 10.x, 12.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - run: npm install 21 | - run: npm test 22 | env: 23 | CI: true 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | build/ 4 | .env 5 | .next/ 6 | 7 | # VSCODE 8 | .vscode/ 9 | 10 | # MACOS 11 | .DS_Store 12 | 13 | .vercel 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Launch Bay Next 2 | ![Typescript](https://github.com/andrelind/launch-bay-next-web/workflows/Typescript/badge.svg?branch=master) 3 | =============== 4 | Another squad builder...? 5 | 6 | Well, I've had the Launch Bay Next mobile app available since the release of X-Wing Second Edition and a bit further down the path I found out that I could reuse most of my components from the app (written in React Native) to be used on the web! 7 | 8 | Said and done, I did a quick conversion of some key components and this is it... 9 | 10 | It's not even close on par feature-wise with the mobile app, but most features will probably be available here also (at some point). 11 | 12 | 13 | Building 14 | ======== 15 | 16 | 1. Install [Node](https://nodejs.org) 17 | 2. Install [Yarn](https://legacy.yarnpkg.com/en/docs/install) 18 | 3. `yarn install` to install all dependencies 19 | 4. `yarn dev` to get the app running 20 | 21 | 22 | Credits 23 | ------- 24 | [X-Wing Miniatures](https://www.fantasyflightgames.com/en/products/x-wing-second-edition/) is made by [Fantasy Flight Games](http://www.fantasyflightgames.com). 25 | -------------------------------------------------------------------------------- /__.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["next/babel"], 3 | "plugins": [ 4 | [ 5 | "module-resolver", 6 | { 7 | "root": ["./"], 8 | "alias": { 9 | "@actions": "./actions", 10 | "@api": "./api", 11 | "@components": "./components", 12 | "@lib": "./lib", 13 | "@reducers": "./reducers", 14 | "@store": "./store" 15 | }, 16 | "extensions": [".js", ".jsx", ".json", ".ts", ".tsx"] 17 | } 18 | ] 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /components/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrelind/launch-bay-next-web/1f83a30c8cfb82fd68dc697c0eebb9caece8bfac/components/.DS_Store -------------------------------------------------------------------------------- /components/ability.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import FormattedText from './formatted-text'; 3 | 4 | type Props = { 5 | ability: { name: string; text: string }; 6 | }; 7 | 8 | export const AbilityComponent = ({ ability }: Props) => { 9 | return ( 10 |
11 | {ability.name}:{' '} 12 | 13 | 14 | 15 |
16 | ); 17 | }; 18 | 19 | export default AbilityComponent; 20 | -------------------------------------------------------------------------------- /components/about.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | type Props = { 4 | onClose: () => void; 5 | }; 6 | 7 | export const AboutComponent: FC = ({ onClose }) => { 8 | return ( 9 |
10 |
11 |

12 | Launch Bay Next 13 |

14 |
15 | 35 |
36 |
37 | 38 |
39 |

Bugs or feature requests

40 | 41 | 42 | Report them here 43 | 44 | 45 |

Like the builder?

46 |

47 | Please consider donating, either via{' '} 48 | Patreon or{' '} 49 | PayPal 50 |

51 |

52 | Want to help out? 53 |

54 | 55 | Web repo 56 | 57 |
58 |
59 | 60 | Core (shared with app) 61 | 62 |
63 |

64 | 65 |

Credits

66 | 81 | 82 |

Trivia

83 |
    84 |
  • Written in Typescript
  • 85 |
  • React frontend (next.js)
  • 86 |
  • GraphQL + Mongo for backend
  • 87 |
88 | 89 |

Legal

90 |

91 | This builder is unofficial and is not affiliated with Fantasy Flight 92 | Games, Lucasfilm Ltd., or Disney. 93 |

94 | Privacy Policy 95 |
96 |
97 | ); 98 | }; 99 | -------------------------------------------------------------------------------- /components/actions.tsx: -------------------------------------------------------------------------------- 1 | import { purple, red } from 'lbn-core/dist/assets/colors'; 2 | import { Action, Difficulty } from 'lbn-core/dist/types'; 3 | import React, { FC } from 'react'; 4 | import XwingFont from './fonts/xwing'; 5 | 6 | type Props = { 7 | actions: Action[]; 8 | vertical?: boolean; 9 | }; 10 | 11 | export const ActionsComponent: FC = ({ actions, vertical }) => { 12 | const color = (difficulty: Difficulty) => { 13 | if (difficulty === 'Red') return red; 14 | if (difficulty === 'Purple') return purple; 15 | return undefined; 16 | }; 17 | 18 | return ( 19 |
20 | {actions.map((a, index) => ( 21 |
25 | 26 | {a.linked && ' -> '} 27 | {a.linked && ( 28 | 32 | )} 33 |
34 | ))} 35 |
36 | ); 37 | }; 38 | 39 | export default ActionsComponent; 40 | -------------------------------------------------------------------------------- /components/base-size.tsx: -------------------------------------------------------------------------------- 1 | import { Size } from "lbn-core/dist/types"; 2 | import React from "react"; 3 | import XWing from "./fonts/xwing"; 4 | 5 | type Props = { 6 | size: Size; 7 | }; 8 | 9 | export const BaseSizeComponent = ({ size }: Props) => { 10 | switch (size) { 11 | case "Small": 12 | return ; 13 | case "Medium": 14 | return ; 15 | default: 16 | return ; 17 | } 18 | }; 19 | 20 | export default BaseSizeComponent; 21 | -------------------------------------------------------------------------------- /components/dial/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from 'react'; 2 | import Row from './row'; 3 | 4 | type Props = { 5 | dial: string[]; 6 | }; 7 | 8 | export const DialComponent: FC = ({ dial }: Props) => { 9 | const rows = ['5', '4', '3', '2', '1', '0']; 10 | const data = rows.map((index) => { 11 | const data = dial.filter((d) => d[0] === index); 12 | if (data.length === 0) { 13 | return null; 14 | } 15 | return ; 16 | }); 17 | 18 | return
{data}
; 19 | }; 20 | 21 | export default memo(DialComponent); 22 | -------------------------------------------------------------------------------- /components/dial/row.tsx: -------------------------------------------------------------------------------- 1 | import { blue, purple, red } from 'lbn-core/dist/assets/colors'; 2 | import React from 'react'; 3 | import XwingFont from '../fonts/xwing'; 4 | 5 | const colorForValue = (value: string) => { 6 | switch (value) { 7 | case 'B': 8 | return blue; 9 | case 'R': 10 | return red; 11 | case 'P': 12 | return purple; 13 | } 14 | }; 15 | 16 | const iconForValue = (d: any) => { 17 | switch (d[1]) { 18 | case 'T': 19 | return { index: 0, data: d[2] + 'turnleft' }; 20 | case 'B': 21 | return { index: 1, data: d[2] + 'bankleft' }; 22 | case 'F': 23 | return { index: 2, data: d[2] + 'straight' }; 24 | case 'N': 25 | return { index: 3, data: d[2] + 'bankright' }; 26 | case 'Y': 27 | return { index: 4, data: d[2] + 'turnright' }; 28 | case 'O': 29 | return { index: 2, data: d[2] + 'stop' }; 30 | case 'K': 31 | return { index: 5, data: d[2] + 'kturn' }; 32 | case 'L': 33 | return { index: 6, data: d[2] + 'sloopleft' }; 34 | case 'P': 35 | return { index: 7, data: d[2] + 'sloopright' }; 36 | case 'E': 37 | return { index: 8, data: d[2] + 'trollleft' }; 38 | case 'R': 39 | return { index: 9, data: d[2] + 'trollright' }; 40 | case 'A': 41 | return { index: 10, data: d[2] + 'reversebankleft' }; 42 | case 'S': 43 | return { index: 11, data: d[2] + 'reversestraight' }; 44 | case 'D': 45 | return { index: 12, data: d[2] + 'reversebankright' }; 46 | default: 47 | // console.log('UNKNOWN MANUEVER', d[1]); 48 | } 49 | }; 50 | 51 | type Props = { 52 | data: string[]; 53 | row: string; 54 | }; 55 | 56 | export const RowComponent = ({ data, row }: Props) => { 57 | const rowData = [ 58 | 'D', 59 | 'D', 60 | 'D', 61 | 'D', 62 | 'D', 63 | 'D', 64 | 'D', 65 | 'D', 66 | 'D', 67 | 'D', 68 | 'D', 69 | 'D', 70 | 'D', 71 | ]; 72 | data.forEach((d) => { 73 | const v = iconForValue(d); 74 | if (v) { 75 | rowData[v.index] = v.data; 76 | } 77 | }); 78 | for (var i = rowData.length; i >= 5; i--) { 79 | if (rowData[i] === 'D') { 80 | rowData.splice(i, 1); 81 | } 82 | } 83 | 84 | return ( 85 |
86 | {row} 87 | {rowData.map((d, index) => { 88 | if (d === 'D') { 89 | return
; 90 | } else { 91 | return ( 92 | 98 | ); 99 | } 100 | })} 101 |
102 | ); 103 | }; 104 | 105 | export default RowComponent; 106 | -------------------------------------------------------------------------------- /components/fonts/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrelind/launch-bay-next-web/1f83a30c8cfb82fd68dc697c0eebb9caece8bfac/components/fonts/.DS_Store -------------------------------------------------------------------------------- /components/fonts/ships.tsx: -------------------------------------------------------------------------------- 1 | import { shipIcons } from "lbn-core/dist/helpers/icon"; 2 | import React from "react"; 3 | 4 | type Props = { 5 | icon?: string; 6 | color?: string; 7 | className?: string; 8 | }; 9 | 10 | export const ShipFont = ({ icon, color, className }: Props) => ( 11 | 12 | {shipIcons(icon || "")} 13 | 14 | ); 15 | 16 | export default ShipFont; 17 | -------------------------------------------------------------------------------- /components/fonts/xwing.tsx: -------------------------------------------------------------------------------- 1 | import { xwingIcons } from 'lbn-core/dist/helpers/icon'; 2 | import React from 'react'; 3 | 4 | type Props = { 5 | icon: string; 6 | color?: string; 7 | className?: string; 8 | }; 9 | 10 | export const XwingFont = ({ icon, color, className }: Props) => ( 11 | 12 | {xwingIcons(icon)} 13 | 14 | ); 15 | 16 | export default XwingFont; 17 | -------------------------------------------------------------------------------- /components/format-error.tsx: -------------------------------------------------------------------------------- 1 | export const FormatError = () => ( 2 |
3 | 10 | 16 | 17 | Not valid in this format 18 |
19 | ); 20 | -------------------------------------------------------------------------------- /components/format.tsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { blue, red, yellow } from 'lbn-core/dist/assets/colors'; 3 | import { Format } from 'lbn-core/dist/types'; 4 | import React from 'react'; 5 | 6 | type Props = { 7 | format: Format; 8 | onClick?: () => any; 9 | }; 10 | 11 | export const colorForFormat = (f: Format) => { 12 | switch (f) { 13 | case 'Extended': 14 | return red; 15 | case 'Standard': 16 | return blue; 17 | case 'Epic': 18 | return yellow; 19 | } 20 | }; 21 | 22 | export const FormatComponent = ({ format, onClick }: Props) => { 23 | const color = colorForFormat(format); 24 | return ( 25 | 26 | {format} 27 | 28 | ); 29 | }; 30 | 31 | export default FormatComponent; 32 | -------------------------------------------------------------------------------- /components/formatted-text.tsx: -------------------------------------------------------------------------------- 1 | import textHelper from "lbn-core/dist/helpers/text"; 2 | import React, { FC } from "react"; 3 | import XwingFont from "./fonts/xwing"; 4 | 5 | type Props = { 6 | text: string; 7 | color?: string; 8 | fontStyle?: "italic" | "normal"; 9 | }; 10 | 11 | export const FormattedText: FC = ({ text, color, fontStyle }) => { 12 | const content = textHelper(text); 13 | 14 | return ( 15 | 21 | {content.map((item, index) => { 22 | switch (item.type) { 23 | case "text": 24 | return item.text; 25 | case "strong": 26 | return ( 27 | 31 | {item.text} 32 | 33 | ); 34 | case "icon": { 35 | return ( 36 | 41 | ); 42 | } 43 | } 44 | })} 45 | 46 | ); 47 | }; 48 | 49 | export default FormattedText; 50 | -------------------------------------------------------------------------------- /components/grants.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | blue, 3 | green, 4 | pink, 5 | purple, 6 | red, 7 | yellow, 8 | } from 'lbn-core/dist/assets/colors'; 9 | import { Difficulty, Grant } from 'lbn-core/dist/types'; 10 | import React from 'react'; 11 | import XwingFont from './fonts/xwing'; 12 | 13 | type Props = { 14 | grants: Grant[]; 15 | }; 16 | 17 | const colorForType = (type: string | void) => { 18 | switch (type) { 19 | case 'attack': 20 | return red; 21 | case 'agility': 22 | return green; 23 | case 'hull': 24 | return yellow; 25 | case 'shields': 26 | return blue; 27 | case 'energy': 28 | return pink; 29 | default: 30 | break; 31 | } 32 | }; 33 | 34 | const colorForDifficulty = (difficulty: Difficulty) => { 35 | if (difficulty === 'Red') return red; 36 | if (difficulty === 'Purple') return purple; 37 | return undefined; 38 | }; 39 | 40 | const mod = (grant: Grant) => { 41 | if (grant.value > 0) { 42 | return `+${grant.value}`; 43 | } 44 | return grant.value; 45 | }; 46 | 47 | const GrantsComponent = ({ grants }: Props) => { 48 | return ( 49 |
50 | {grants.map((grant, index) => ( 51 |
52 | {grant.slot && } 53 | {grant.stat && ( 54 |
55 | 59 | {mod(grant)} 60 | 61 | 62 |
63 | )} 64 | {grant.action && ( 65 |
66 | 70 | {grant.action.linked && ' -> '} 71 | {grant.action.linked && ( 72 | 76 | )} 77 |
78 | )} 79 |
80 | ))} 81 |
82 | ); 83 | }; 84 | 85 | export default GrantsComponent; 86 | -------------------------------------------------------------------------------- /components/import.tsx: -------------------------------------------------------------------------------- 1 | import { serializer } from 'lbn-core/dist/helpers'; 2 | import { canImportXws } from 'lbn-core/dist/helpers/import+export'; 3 | import { SquadronXWS } from 'lbn-core/dist/types'; 4 | import { useRouter } from 'next/router'; 5 | import { FC, useState } from 'react'; 6 | 7 | type Props = { 8 | onClose: () => void; 9 | }; 10 | 11 | export const ImportComponent: FC = ({ onClose }) => { 12 | const [error, setError] = useState(); 13 | const [xws, setXws] = useState(); 14 | 15 | const router = useRouter(); 16 | 17 | const handleSubmit = async (e: React.FormEvent) => { 18 | e.preventDefault(); 19 | 20 | if (!xws) { 21 | return; 22 | } 23 | 24 | router.push(`/?lbx=${serializer.serialize(xws)}`); 25 | onClose(); 26 | }; 27 | 28 | return ( 29 |
30 |
31 |
32 |
33 |

34 | Import XWS 35 |

36 |

37 | XWS is a common format shared by many squadron builders for 38 | X-Wing. 39 |

40 |
41 | 42 |
43 |
44 |
45 |