├── .firebaserc ├── .gitignore ├── LICENSE ├── README.md ├── firebase.json ├── jpvcdb-landing-full.jpg ├── package-lock.json ├── package.json └── src ├── app ├── .babelrc ├── admin │ ├── Companies.js │ ├── Header.js │ ├── Messages.js │ └── Users.js ├── asserts │ ├── antd-custom.less │ └── styles.less ├── components │ ├── App.js │ ├── Banner.js │ ├── BannerMin.js │ ├── CompanyAnalysis.js │ ├── CompanyDetails.js │ ├── Contact.js │ ├── Feedback.js │ ├── Footer.js │ ├── Header.js │ ├── Map.js │ ├── Masthead.js │ ├── Page1.js │ ├── Page2.js │ ├── Page3.js │ └── RankingTable.js ├── layout │ ├── About.js │ ├── Cohort.js │ ├── Company.js │ ├── Home.js │ ├── Login.js │ ├── Ranking.js │ └── static │ │ ├── default.less │ │ ├── footer.less │ │ ├── header.less │ │ ├── home.less │ │ ├── responsive.less │ │ └── style.js ├── lib │ ├── AuthStateProvider.js │ ├── FirebaseProvider.js │ ├── auth.js │ ├── createCompany.js │ ├── createMessage.js │ ├── emptyMessage.js │ ├── firebaseManager.js │ └── redirect.js ├── next-seo.config.js ├── next.config.js ├── pages │ ├── about.js │ ├── cohort.js │ ├── company.js │ ├── dashboard.js │ ├── index.js │ ├── login.js │ └── ranking.js ├── scripts │ ├── README.md │ ├── add-airtable-records.js │ ├── add-seed-posts.js │ ├── airtable-migration.js │ ├── alexa-migration.js │ ├── algolia-migration.js │ ├── data │ │ ├── seed-companies.json │ │ └── seed-posts.json │ ├── generate-analytics.js │ ├── generate-ranking.js │ ├── package-lock.json │ ├── package.json │ └── sample-migration.js └── static │ ├── header-hero.jpg │ ├── header-hero2.jpg │ ├── header-hero3.jpg │ ├── laptop.svg │ ├── laptop2.svg │ ├── logo-clear.png │ ├── logo-clear@2x.png │ ├── logo-white.png │ ├── logo-white@2x.png │ └── logo-word-white.png ├── functions ├── .babelrc └── index.js └── public ├── favicon.ico └── placeholder.html /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "jpvcdb" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | credentials/ 4 | .firebase/ 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) {{ year }} Mobile Flow LLC 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 14 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 15 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 16 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 18 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 19 | OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | JP.VC.DB 2 | ======== 3 | 4 | ### MobileFlow Next Firebase Ant Design Template 5 | 6 | An open source clone the very popular [ycdb](https://ycdb.co) to encourage similary investment visibility and anlaysis 7 | for other investment organizations or, in our case, whole countries. SEO optimized, performant, opinionated React/Next/AntDesign/Firebase template app with fast loading landing page with lazy-loading of Firebase data 8 | using a simple provider class. Demo available [here](https://jpvcdb.firebaseapp.com/). 9 | 10 |

11 | 12 |

13 | 14 | With code and inspiration from Sam Lolla [Firefly](http://getfirefly.org/) and [react-firestore](https://github.com/green-arrow/react-firestore/blob/master/src/FirestoreCollection.js), with design support from [ManyPixkes](https://manypixels.co/). 15 | 16 | 17 | Based on `create-next-app` and initially built using `with-firebase-hosting` 18 | 19 | ```bash 20 | npx create-next-app --example with-firebase-hosting with-firebase-hosting-app 21 | ``` 22 | 23 | 24 |
25 | Download manually 26 | 27 | Download the example: 28 | 29 | ```bash 30 | curl https://codeload.github.com/zeit/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/with-firebase-hosting 31 | cd with-firebase-hosting 32 | ``` 33 | 34 |
35 | 36 |
37 | Set up firebase 38 | 39 | * install Firebase Tools: `npm i -g firebase-tools` 40 | * create a project through the [firebase web console](https://console.firebase.google.com/) 41 | * grab the projects ID from the web consoles URL: `https://console.firebase.google.com/project/` 42 | * update the `.firebaserc` default project ID to the newly created project 43 | * login to the Firebase CLI tool with `firebase login` 44 | 45 |
46 | 47 |
48 | Install Project 49 | 50 | ```bash 51 | npm install 52 | ``` 53 | 54 | #### Run Next.js development: 55 | 56 | ```bash 57 | npm run dev 58 | ``` 59 | 60 | #### Run Firebase locally for testing: 61 | 62 | ``` 63 | npm run serve 64 | ``` 65 | 66 | #### Deploy it to the cloud with Firebase: 67 | 68 | ```bash 69 | npm run deploy 70 | ``` 71 | 72 | #### Clean dist folder 73 | 74 | ```bash 75 | npm run clean 76 | ``` 77 | 78 |
79 | 80 |
81 | Bitbucket repo 82 | 83 | Clone from Bitbucket repo 84 | 85 | ``` 86 | git remote add bitbucket git@bitbucket.org:mobileflowllc/jpvcdb.git 87 | git push -u bitbucket master 88 | ``` 89 |
90 | 91 | 92 | ## Serverless hosting with Firebase 93 | 94 | Using Firebase hosting with NextJS generated SSR design files. This should allow for easier setup (add new pages via `/pages` subdirectory) and better SEO (using `next-seo` to configure, but also SSR generates 'static' home/blog/faq pages as needed). 95 | 96 | 97 | 98 | ### Customization 99 | 100 | Ant Design themeing for both landing page and application controls. Use the [Ant Design](https://ant.design/components])summary page and [style sheet guide](https://github.com/ant-design/ant-design/blob/master/components/style/themes/default.less) to customize the look & feel. 101 | 102 | The directory structure in the `src/app` folder is as follows: 103 | 104 | ```bash 105 | |____pages 106 | | |____about.js 107 | | |____index.js 108 | | |____login.js 109 | | |____dashboard.js 110 | |____credentials 111 | | |____client.js 112 | |____landing 113 | | |____Home.js 114 | | |____Header.js 115 | | |____Banner.js 116 | | |____Page1.js 117 | | |____static 118 | | | |____style.js 119 | | | |____default.less 120 | | | |____home.less 121 | | | |____footer.less 122 | | | |____header.less 123 | | | |____responsive.less 124 | |____components 125 | | |____App.js 126 | | |____Header.js 127 | | |____Home.js 128 | | |____Login.js 129 | |____asserts 130 | | |____styles.less 131 | | |____antd-custom.less 132 | |____static 133 | | |____logo-word-white.png 134 | |____lib 135 | | |____redirect.js 136 | | |____auth.js 137 | | |____firebaseManager.js 138 | |____next.config.js 139 | |____next-seo.config.js 140 | |____scripts 141 | ``` 142 | 143 | 144 | The Firebase configuration is contained in `credentials/client.js` and is used the by the `firebaseManager.js` singleton to manage authorization, etc. 145 | 146 | The home / landing page is `index.js` by default, and loads an Ant Design stylized landing page from `landing/Home.js` with custom `less` styling from within the `landing/static` directory. 147 | 148 | The application page is `dashboard` and contains custom application components from the `components` directory. Current functionality is limited to allowing admins to create and edit company data. Extensive use of Firebase admin scripts is made to migrate and shape data. See the `scripts` directory. 149 | 150 | Both the application and landing page use the `@zeit/next-less` loading methods as specified within `next.config.js` file, which loads the default Ant Design style sheets and applies overrides using the files contained within `asserts/antd-custom.less`. 151 | 152 | 153 | ### TODO 154 | 155 | 1. Clean up admin CRUD functions, issues with AntD image uploader component within forms 156 | 2. Add [i18next language support](https://react.i18next.com) using HOC for AntD 157 | 3. Populate data with Japanese startup cohort data 158 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "dist/public", 4 | "rewrites": [ 5 | { 6 | "source": "**/**", 7 | "function": "next" 8 | } 9 | ] 10 | }, 11 | "functions": { 12 | "source": "dist/functions" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /jpvcdb-landing-full.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moflo/jpvcdb/fe87bd2d73f79d72bc600c695b5575e7600ead41/jpvcdb-landing-full.jpg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "with-firebase-hosting", 3 | "version": "4.0.1", 4 | "description": "Host Next.js SSR app on Firebase Cloud Functions with Firebase Hosting redirects.", 5 | "scripts": { 6 | "dev": "next \"src/app/\"", 7 | "preserve": "npm run build-public && npm run build-funcs && npm run build-app && npm run copy-deps && npm run install-deps", 8 | "serve": "NODE_ENV=production firebase serve", 9 | "predeploy": "npm run build-public && npm run build-funcs && npm run build-app && npm run copy-deps", 10 | "deploy": "firebase deploy", 11 | "clean": "rimraf \"dist/functions/**\" && rimraf \"dist/public\"", 12 | "build-public": "cpx \"src/public/**/*.*\" \"dist/public\" -C && cpx \"src/app/static/*.*\" \"dist/public/static\" -C ", 13 | "build-funcs": "babel \"src/functions\" --out-dir \"dist/functions\"", 14 | "build-app": "next build \"src/app/\"", 15 | "copy-deps": "cpx \"*{package.json,package-lock.json,yarn.lock}\" \"dist/functions\" -C", 16 | "install-deps": "cd \"dist/functions\" && npm i" 17 | }, 18 | "dependencies": { 19 | "@zeit/next-less": "^1.0.1", 20 | "antd": "^3.11.2", 21 | "babel-plugin-import": "^1.7.0", 22 | "firebase": "^5.6.0", 23 | "firebase-admin": "^6.3.0", 24 | "firebase-functions": "^2.1.0", 25 | "less": "3.0.4", 26 | "less-vars-to-js": "1.3.0", 27 | "next": "7.0.2", 28 | "next-seo": "^1.2.0", 29 | "react": "16.7.0", 30 | "react-dom": "16.7.0", 31 | "styled-components": "^4.1.1", 32 | "prop-types": "15.6.2", 33 | "@babel/runtime": "^7.2.0", 34 | "mapbox-gl": "^0.51.0", 35 | "path-match": "1.2.4", 36 | "algoliasearch": "^3.32.0", 37 | "slugify": "^1.3.4" 38 | }, 39 | "devDependencies": { 40 | "@babel/cli": "^7.2.0", 41 | "cpx": "1.5.0", 42 | "firebase-tools": "6.1.2", 43 | "prettier": "1.12.1", 44 | "rimraf": "2.6.2", 45 | "@babel/plugin-proposal-decorators": "^7.1.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/app/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["next/babel"]] 3 | } 4 | -------------------------------------------------------------------------------- /src/app/admin/Companies.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import Link from 'next/link' 3 | import { Layout, Breadcrumb, Button, Table, Tabs, Card, Form, Input, Upload, Modal, Select, Icon, Tag, message, notification } from 'antd'; 4 | import styled from 'styled-components'; 5 | import FirebaseProvider from '../lib/FirebaseProvider'; 6 | import createCompany from '../lib/createCompany' 7 | import firebaseManager from '../lib/firebaseManager' 8 | 9 | const { Content } = Layout; 10 | const FormItem = Form.Item; 11 | const TabPane = Tabs.TabPane; 12 | 13 | class PhotoUpload extends React.Component { 14 | static getDerivedStateFromProps(nextProps) { 15 | // Should be a controlled component. 16 | if ('value' in nextProps) { 17 | return { 18 | ...(nextProps.value || {}), 19 | }; 20 | } 21 | return null; 22 | } 23 | 24 | constructor(props) { 25 | super(props); 26 | // console.log(`PhotoUpload, props: ${JSON.stringify(props)}`) 27 | 28 | const value = props.value || {}; 29 | const fileList = props.url ? [{uid: '-1', name: 'image.png', status: 'done', url: props.url}] : [] 30 | this.state = { 31 | previewVisible: false, 32 | previewImage: '', 33 | fileList: fileList, 34 | file: value.file, 35 | percent: 0, 36 | }; 37 | } 38 | 39 | beforeUpload = (file) => { 40 | this.setState({ file }) 41 | this.triggerChange({ file }); 42 | return true 43 | } 44 | 45 | triggerChange = (changedValue) => { 46 | // Should provide an event to pass value to Form. 47 | const onChange = this.props.onChange; 48 | if (onChange) { 49 | console.log(`triggerChange: ${JSON.stringify(changedValue)}`) 50 | // onChange(Object.assign({}, this.state, changedValue)); 51 | onChange(changedValue); 52 | } 53 | } 54 | 55 | normFile = (e) => { 56 | console.log('Upload event:', e); 57 | if (Array.isArray(e)) { 58 | return e; 59 | } 60 | return e && e.fileList; 61 | } 62 | 63 | handlePreview = (file) => { 64 | this.setState({ 65 | previewImage: file.url || file.thumbUrl, 66 | previewVisible: true, 67 | }); 68 | } 69 | 70 | handleCancelPreview = () => this.setState({ previewVisible: false }) 71 | 72 | handleChange = ({ fileList }) => { 73 | this.setState({ fileList }); 74 | if (fileList.length == 0) { 75 | this.triggerChange({ file: null }); 76 | } 77 | }; 78 | 79 | render() { 80 | const { size } = this.props; 81 | const { previewVisible, previewImage, fileList } = this.state; 82 | const uploadButton = ( 83 |
84 | 85 |
Upload
86 |
87 | ); 88 | 89 | return ( 90 |
91 | 98 | {fileList.length >= 1 ? null : uploadButton} 99 | 100 | 101 | example 102 | 103 |
104 | ); 105 | } 106 | } 107 | 108 | 109 | class CompanyCreate extends React.Component { 110 | state = { 111 | cancelCallback: this.props.cancelCallback, 112 | deploying: false, 113 | menuVisible: false, 114 | loading: false, 115 | percent: 0 116 | } 117 | 118 | handleOk = () => { 119 | this.setState({ 120 | menuVisible: false, 121 | }); 122 | } 123 | 124 | handleSubmit = (e) => { 125 | e.preventDefault(); 126 | const {history} = this.props; 127 | this.props.form.validateFields((err, values) => { 128 | if (!err) { 129 | console.log("handleSubmit values:", JSON.stringify(values)) 130 | 131 | var promises = [] 132 | 133 | let landingFile = values.landingpage 134 | 135 | var hide = message.loading(`Loading... ${landingFile.name || landingFile}`, this.state.percent); 136 | 137 | if (typeof(landingFile) === "string") { 138 | promises.push( Promise.resolve(landingFile) ) 139 | } 140 | else { 141 | // Note the validateFields values for uploads returns an rc-upload object, https://www.npmjs.com/package/rc-upload 142 | // The JS File object is therefore landingFile.file 143 | let uploadLanding = firebaseManager.sharedInstance.uploadFile(landingFile.file,'landingpage.png',this.uploadProgress) 144 | promises.push( uploadLanding ) 145 | } 146 | 147 | let iconFile = values.icon 148 | 149 | var hide2 = message.loading(`Loading icon... ${iconFile.name || iconFile}`, this.state.percent); 150 | 151 | if (typeof(iconFile) === "string") { 152 | promises.push( Promise.resolve(iconFile) ) 153 | } 154 | else { 155 | // Note the validateFields values for uploads returns an rc-upload object, https://www.npmjs.com/package/rc-upload 156 | // The JS File object is therefore iconFile.file 157 | let uploadIcon = firebaseManager.sharedInstance.uploadFile(iconFile.file,'icon.png',this.uploadProgress) 158 | promises.push( uploadIcon ) 159 | } 160 | 161 | Promise.all(promises) 162 | .then((result) => { 163 | console.log("uploadFile result", result) 164 | 165 | values.landingpage = result[0] 166 | values.icon = result[1] 167 | values.logo = result[1] 168 | 169 | hide() 170 | hide2() 171 | 172 | return createCompany(values) 173 | }) 174 | .then( (resp) => { 175 | 176 | if (resp) { 177 | notification.info({ 178 | message: "Company Created!", 179 | description: `Thank you ${values.name}. We crteated a new company.` 180 | }) 181 | 182 | this.props.form.resetFields() 183 | 184 | this.state.cancelCallback && this.state.cancelCallback() 185 | 186 | } 187 | }) 188 | .catch( err => { 189 | var errorCode = err.code || 'Sorry, there was a problem.'; 190 | var errorMessage = err.message || 'Please correct the errors and submit again' 191 | 192 | notification.error({ 193 | message: errorCode, 194 | description: errorMessage 195 | }) 196 | 197 | console.log(`Error saving company: ${errorMessage}`); 198 | 199 | }) 200 | 201 | 202 | } 203 | else { 204 | var errorCode = err.code || 'Sorry, there was a problem.'; 205 | var errorMessage = err.message || 'Please correct the errors and submit again' 206 | 207 | notification.error({ 208 | message: errorCode, 209 | description: errorMessage 210 | }) 211 | } 212 | }) 213 | } 214 | 215 | normFile = (e) => { 216 | console.log('Upload event:', e); 217 | if (Array.isArray(e)) { 218 | return e; 219 | } 220 | return e && e.fileList; 221 | } 222 | 223 | uploadProgress = (percent,task) => { 224 | console.log(`uploadProgress: progress = ${percent}`) 225 | this.setState({ percent }) 226 | } 227 | 228 | // Testing validation 229 | validateUpload = (rule, value, callback) => { 230 | console.log(`validateUpload: ${JSON.stringify(value)}`) 231 | if (true) { //(value && value.file) { 232 | callback(); 233 | return; 234 | } 235 | 236 | callback && callback('Need to add image') 237 | } 238 | 239 | onChangeIcon = value => { console.log(`changeIcon: ${JSON.stringify(value)}`)} 240 | 241 | render() { 242 | const { getFieldDecorator } = this.props.form; 243 | const { deploying, loading } = this.state; 244 | const uploadButton = ( 245 |
246 | 247 |
Upload
248 |
249 | ); 250 | 251 | return ( 252 | 253 | 254 | 255 |
256 | 257 |
258 | 259 | 260 | 261 | 262 | {getFieldDecorator('name', { 263 | rules: [{ required: true, message: 'Please input a name!' }], 264 | })( 265 | } placeholder=" New Company" /> 266 | )} 267 | 268 | 269 | 270 | {getFieldDecorator('description', { 271 | rules: [{ required: true, message: 'Please input your description!' }], 272 | })( 273 | } type="text" placeholder=" Description..." /> 274 | )} 275 | 276 | 277 | 278 | {getFieldDecorator('batch', { 279 | rules: [{ required: true, message: 'Please input your batch!' }], 280 | })( 281 | } type="text" placeholder=" Batch" /> 282 | )} 283 | 284 | 285 | 286 | {getFieldDecorator('category', { 287 | rules: [{ required: true, message: 'Please input your category!' }], 288 | initialValue: 'Other SaaS', 289 | 290 | })( 291 | 310 | 311 | )} 312 | 313 | 314 | 315 | 316 | {getFieldDecorator('status', { 317 | rules: [{ required: true, message: 'Please input company status!' }], 318 | initialValue: 'Live', 319 | 320 | })( 321 | 326 | 327 | )} 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | {getFieldDecorator('funding', { 337 | initialValue: 0 338 | })( 339 | } placeholder=" Funding" /> 340 | )} 341 | 342 | 343 | 344 | {getFieldDecorator('exit', { 345 | initialValue: 0 346 | })( 347 | } placeholder=" Exit" /> 348 | )} 349 | 350 | 351 | 352 | {getFieldDecorator('employees', { 353 | initialValue: 1 354 | })( 355 | } placeholder=" Employees" /> 356 | )} 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | {getFieldDecorator('address', { 365 | initialValue: "" 366 | })( 367 | } placeholder=" Address" /> 368 | )} 369 | 370 | 371 | 372 | {getFieldDecorator('www', { 373 | initialValue: 'http://' 374 | })( 375 | } placeholder=" Website" /> 376 | )} 377 | 378 | 379 | 380 | {getFieldDecorator('hqLocation', { 381 | initialValue: '' 382 | })( 383 | } placeholder=" Location" /> 384 | )} 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | {getFieldDecorator('landingpage', { 395 | valuePropName: 'url', 396 | // getValueFromEvent: this.normFile, 397 | initialValue: { file: null }, 398 | rules: [{ validator: this.validateUpload }] 399 | })( )} 400 | 401 | 402 | 403 | 404 | 405 | 406 | {getFieldDecorator('icon', { 407 | valuePropName: 'url', 408 | // getValueFromEvent: this.normFile, 409 | initialValue: { file: null }, 410 | rules: [{ validator: this.validateUpload }] 411 | })( )} 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 422 | 423 | 424 |
425 |
426 |
427 |
428 | ) 429 | } 430 | } 431 | 432 | export default class MFCompanies extends React.Component { 433 | state = { 434 | deploying: false, 435 | menuVisible: false, 436 | loading: false, 437 | limit: 10, 438 | fields: {} // Form props, same as record 439 | } 440 | 441 | handleLoadMore = () => { 442 | this.setState({ 443 | limit: this.state.limit + 10 444 | }) 445 | } 446 | 447 | handleShowMenu = () => { 448 | this.setState({ 449 | menuVisible: true, 450 | }); 451 | } 452 | 453 | handleHideMenu = () => { 454 | this.setState({ 455 | menuVisible: false, 456 | }); 457 | } 458 | 459 | handleOk = () => { 460 | this.setState({ 461 | menuVisible: false, 462 | }); 463 | } 464 | 465 | onEdit = record => { 466 | this.setState({ 467 | fields: record, 468 | menuVisible: true 469 | }) 470 | } 471 | 472 | onRowSelect = record => { 473 | // console.log("Select record ", record) 474 | this.onEdit(record) 475 | } 476 | render() { 477 | const { deploying, loading, limit, fields } = this.state; 478 | const WrappedCompanyCreate = Form.create({mapPropsToFields(props) { 479 | return { 480 | name: Form.createFormField({ value: props.name }), 481 | description: Form.createFormField({ value: props.description }), 482 | batch: Form.createFormField({ value: props.batch }), 483 | category: Form.createFormField({ value: props.category }), 484 | status: Form.createFormField({ value: props.status }), 485 | funding: Form.createFormField({ value: props.funding }), 486 | exit: Form.createFormField({ value: props.exit }), 487 | employees: Form.createFormField({ value: props.employees }), 488 | address: Form.createFormField({ value: props.address }), 489 | www: Form.createFormField({ value: props.www }), 490 | hqLocation: Form.createFormField({ value: props.hqLocation }), 491 | landingpage: Form.createFormField({ value: props.landingpage }), 492 | icon: Form.createFormField({ value: props.logo}), 493 | }; 494 | }})(CompanyCreate); 495 | 496 | const colorForStatus = status => { 497 | if (status.match(/live/i)) return "#ffc108" // yellow 498 | if (status.match(/dead/i)) return "#dc3545" // red 499 | if (status.match(/exit/i)) return "#28a745" // green 500 | return "gray" 501 | } 502 | 503 | const columns = [{ 504 | title: 'Name', 505 | dataIndex: 'id', 506 | key: 'id', 507 | // render: ((text) => {text}), 508 | }, { 509 | title: 'Batch', 510 | dataIndex: 'batch', 511 | key: 'batch', 512 | }, { 513 | title: 'Status', 514 | dataIndex: 'status', 515 | key: 'status', 516 | align: 'center', 517 | render: ((tag) => {tag.toUpperCase()}) 518 | }, { 519 | title: 'Category', 520 | dataIndex: 'category', 521 | key: 'category', 522 | }, { 523 | title: 'Actions', 524 | // dataIndex: 'databaseURL', 525 | key: 'edit', 526 | render: ((text,record) => 547 | 548 |
549 | 550 | 551 | 552 | { ({error, isLoading, data}) => { 553 | 554 | if (error) { console.error("Error loading users ", error)} 555 | 556 | return( 557 | record.id} 561 | onRow={(record) => ({ 562 | onClick: () => { this.onRowSelect(record); } 563 | })} 564 | loading={isLoading} 565 | pagination={true} /> 566 | ) 567 | }} 568 | 569 | 570 | 571 |
572 | 573 |
574 | 575 | 576 | } 577 | 578 | ) 579 | 580 | } 581 | 582 | } -------------------------------------------------------------------------------- /src/app/admin/Header.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import Link from 'next/link' 3 | import { Layout, Menu, Divider, Avatar, Modal, Table } from 'antd'; 4 | import styled from 'styled-components'; 5 | import firebaseManager from '../lib/firebaseManager' 6 | import redirect from "../lib/redirect"; 7 | 8 | 9 | const { Header } = Layout; 10 | 11 | const UserName = styled.span` 12 | margin-left: 8px; 13 | ` 14 | 15 | const AvatarWithIcon = styled(Avatar)` 16 | .anticon { 17 | margin-right: 0 !important; 18 | } 19 | ` 20 | 21 | export default class MFHeader extends React.Component { 22 | state = { 23 | visible: false, 24 | user: firebaseManager.sharedInstance.getUserDetails(), 25 | deploying: false, 26 | isMoblie: false 27 | } 28 | 29 | headerMenuOnClick = (menuItem) => { 30 | const {history} = this.props; 31 | if(menuItem.key === 'logout') { 32 | redirect('/'); 33 | } else if(menuItem.key === "overview") { 34 | this.setState({ 35 | visible: true 36 | }); 37 | } 38 | } 39 | 40 | hideOverview = () => { 41 | this.setState({ 42 | visible: false, 43 | }); 44 | } 45 | 46 | 47 | render() { 48 | const {visible, user, deploying, isMoblie} = this.state; 49 | 50 | const menuMode = isMoblie ? 'inline' : 'horizontal'; 51 | 52 | const username = user ? user.name || user.email : 'loading...'; 53 | 54 | const columns = [{ 55 | title: 'Name', 56 | dataIndex: 'name', 57 | key: 'name', 58 | }, { 59 | title: 'Email', 60 | dataIndex: 'email', 61 | key: 'email', 62 | }, { 63 | title: 'Avatar URL', 64 | dataIndex: 'avatarURL', 65 | key: 'avatarURL', 66 | render: ((text) => ), 67 | }]; 68 | 69 | 70 | return ( 71 |
72 | 73 | 74 | Help 75 | 76 | 77 | {user ? 78 | : 79 | } 80 | {username} 81 | }> 82 | Overview 83 | Log out 84 | 85 | 86 | 93 |
94 | 95 | 96 | ) 97 | 98 | } 99 | 100 | } -------------------------------------------------------------------------------- /src/app/admin/Messages.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import Link from 'next/link' 3 | import { Layout, Breadcrumb, Button, Table, Modal } from 'antd'; 4 | import styled from 'styled-components'; 5 | import FirebaseProvider from '../lib/FirebaseProvider'; 6 | 7 | 8 | const { Content } = Layout; 9 | 10 | 11 | export default class MFMessages extends React.Component { 12 | state = { 13 | deploying: false, 14 | menuVisible: false, 15 | loading: false, 16 | record: {} 17 | } 18 | 19 | handleShowMenu = () => { 20 | console.log("handleShowMenu") 21 | this.setState({ 22 | menuVisible: true, 23 | }); 24 | } 25 | 26 | handleHideMenu = () => { 27 | this.setState({ 28 | menuVisible: false, 29 | }); 30 | } 31 | 32 | handleOk = () => { 33 | this.setState({ 34 | menuVisible: false, 35 | }); 36 | } 37 | 38 | handleEdit = (e,record) => { 39 | this.setState({ 40 | record, 41 | menuVisible: true 42 | }) 43 | } 44 | 45 | onRowSelect = record => { 46 | console.log("Select record ", record) 47 | } 48 | 49 | render() { 50 | const { deploying, loading, record } = this.state; 51 | 52 | const columns = [{ 53 | title: 'Name', 54 | dataIndex: 'name', 55 | key: 'name', 56 | }, { 57 | title: 'Email', 58 | dataIndex: 'email', 59 | key: 'email', 60 | }, { 61 | title: 'Problem', 62 | dataIndex: 'problem', 63 | key: 'peoblem', 64 | }, { 65 | title: 'Company', 66 | dataIndex: 'company', 67 | key: 'company', 68 | render: ((text) => {text}), 69 | }, { 70 | title: 'Message', 71 | dataIndex: 'message', 72 | key: 'message', 73 | width: 450 74 | }, { 75 | title: 'Actions', 76 | // dataIndex: 'databaseURL', 77 | key: 'edit', 78 | render: ((text,record) => 93 | 94 | 95 | 96 | 97 | { ({error, isLoading, data}) => { 98 | 99 | if (error) { console.error("Error loading users ", error)} 100 | 101 | return( 102 |
record.id} 106 | onRow={(record) => ({ 107 | onClick: () => { this.onRowSelect(record); } 108 | })} 109 | loading={isLoading} 110 | pagination={true} /> 111 | ) 112 | }} 113 | 114 | 115 | 116 | 117 | Cancel, 124 | , 127 | ]} 128 | > 129 |
130 | 131 | 132 | 133 | ) 134 | 135 | } 136 | 137 | } -------------------------------------------------------------------------------- /src/app/admin/Users.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import Link from 'next/link' 3 | import { Layout, Breadcrumb, Avatar, Table } from 'antd'; 4 | import styled from 'styled-components'; 5 | import FirebaseProvider from '../lib/FirebaseProvider'; 6 | 7 | const { Content } = Layout; 8 | 9 | 10 | export default class MFUsers extends React.Component { 11 | state = { 12 | deploying: false, 13 | } 14 | 15 | onRowSelect = record => { 16 | console.log("Select record ", record) 17 | } 18 | 19 | render() { 20 | const { deploying } = this.state; 21 | 22 | const columns = [{ 23 | title: 'Name', 24 | dataIndex: 'name', 25 | key: 'name', 26 | }, { 27 | title: 'Email', 28 | dataIndex: 'email', 29 | key: 'email', 30 | }, { 31 | title: 'User ID', 32 | dataIndex: 'id', 33 | key: 'id', 34 | }, { 35 | title: 'Avatar URL', 36 | dataIndex: 'avatarURL', 37 | key: 'avatarURL', 38 | render: ((text) => {text}), 39 | }]; 40 | 41 | 42 | return ( 43 | 44 | 45 | 46 | Home 47 | Users 48 | 49 | 50 | 51 | 52 | { ({error, isLoading, data}) => { 53 | 54 | if (error) { console.error("Error loading users ", error)} 55 | 56 | return( 57 |
58 |
record.id} 62 | onRow={(record) => ({ 63 | onClick: () => { this.onRowSelect(record); } 64 | })} 65 | loading={isLoading} 66 | pagination={true} /> 67 | 68 | ) 69 | }} 70 | 71 | 72 | 73 | ) 74 | 75 | } 76 | 77 | } -------------------------------------------------------------------------------- /src/app/asserts/antd-custom.less: -------------------------------------------------------------------------------- 1 | // @import (css) url('https://fonts.googleapis.com/css?family=Roboto+Slab'); 2 | 3 | @primary-color : #ed2939; 4 | @font-family : "Avenir Next", Sans-Serif; 5 | @font-size-base : 14px; 6 | @font-size-sm : 12px; 7 | 8 | @layout-header-height: 83px; 9 | @border-radius-base: 2px; 10 | -------------------------------------------------------------------------------- /src/app/asserts/styles.less: -------------------------------------------------------------------------------- 1 | @import "~antd/dist/antd.less"; 2 | @import "./antd-custom.less"; 3 | -------------------------------------------------------------------------------- /src/app/components/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Header from './Header' 3 | import NextSeo from 'next-seo'; 4 | import Router from "next/router"; 5 | 6 | import '../asserts/styles.less' 7 | import SEO from '../next-seo.config'; 8 | 9 | Router.events.on('routeChangeComplete', () => { window.scrollTo(0, 0); }); 10 | 11 | const App = ({ children }) => ( 12 |
13 | 14 | {children} 15 |
16 | ) 17 | 18 | export default App 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/app/components/Banner.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Spin, Select, Button } from 'antd'; 4 | import Link from 'next/link' 5 | import Router from "next/router"; 6 | import algoliasearch from 'algoliasearch'; 7 | const algoliaAccount = require('../credentials/algoliaAccountKey.json') 8 | var client = algoliasearch(algoliaAccount.app_id, algoliaAccount.api_key); 9 | var algolia = client.initIndex('companies'); 10 | import styled from 'styled-components'; 11 | 12 | 13 | const SearchContainer = styled.div` 14 | margin-top: 30px; 15 | 16 | .ant-select-selection { 17 | background: #FF6464; 18 | border: 1px solid #C80018; 19 | color: #ffffff; 20 | } 21 | 22 | .ant-select-selection__placeholder { 23 | color: #ffffff; 24 | } 25 | 26 | ` 27 | const Option = Select.Option; 28 | 29 | class Banner extends React.PureComponent { 30 | static propTypes = { 31 | onEnterChange: PropTypes.func, 32 | } 33 | state = { 34 | onEnterChange: this.props.onEnterChange, 35 | value: "", 36 | data: [], 37 | fetching: false, 38 | isMobile: false, 39 | }; 40 | 41 | searchDone = (err, content) => { 42 | if (err) console.log('searchDone, error: '+ err ) 43 | 44 | const data = content.hits.map( co => ({ 45 | name: `${co.name} - ${co.description}`, 46 | key: co.objectID, 47 | })); 48 | 49 | this.setState({ data, fetching: false }); 50 | } 51 | 52 | handleSearch = (value) => { 53 | console.log('fetching company', value); 54 | this.setState({ data: [], fetching: true }); 55 | algolia.search({ 56 | query: value, 57 | hitsPerPage: 10 58 | }, 59 | this.searchDone 60 | ) 61 | } 62 | 63 | handleChange = (value) => { 64 | this.setState({ 65 | value, 66 | data: [], 67 | fetching: false, 68 | }); 69 | } 70 | 71 | onSelect = (value) => { 72 | console.log(value) 73 | Router.push('/company?id='+value,'/company/'+value) 74 | } 75 | 76 | selectRandomCo = () => { 77 | let companies = ["airbnb","aalo","aerones","airship","beanstalk"] 78 | let co = companies[Math.floor(Math.random()*companies.length)]; 79 | this.onSelect(co) 80 | } 81 | 82 | render() { 83 | const { fetching, data, value } = this.state; 84 | 85 | return ( 86 |
87 |
88 |

JP.VC.DB

89 | 90 | 91 | 105 |
106 | 107 | 108 |
109 |
110 |
111 | ); 112 | 113 | } 114 | } 115 | 116 | export default Banner; -------------------------------------------------------------------------------- /src/app/components/BannerMin.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | 5 | 6 | class BannerMin extends React.PureComponent { 7 | static propTypes = { 8 | onEnterChange: PropTypes.func, 9 | } 10 | state = { 11 | onEnterChange: this.props.onEnterChange, 12 | isMobile: false, 13 | }; 14 | 15 | 16 | render() { 17 | 18 | return ( 19 |
20 |
21 |

JP.VC.DB

22 |
23 |
24 | ); 25 | 26 | } 27 | } 28 | 29 | export default BannerMin; -------------------------------------------------------------------------------- /src/app/components/CompanyAnalysis.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Router from "next/router"; 4 | import { Card, Row, Col, Progress, Icon, Tabs } from 'antd'; 5 | import styled from 'styled-components'; 6 | 7 | const TabPane = Tabs.TabPane 8 | const { Meta } = Card; 9 | 10 | 11 | const AnalysisContainer = styled.div` 12 | padding-top: 30px; 13 | padding-bottom: 30px; 14 | background-color: #f8f9fa; 15 | .ant-tabs-card > .ant-tabs-content { 16 | margin-top: -16px; 17 | } 18 | 19 | .ant-tabs-card > .ant-tabs-content > .ant-tabs-tabpane { 20 | background: #fff; 21 | padding: 16px; 22 | } 23 | 24 | .ant-tabs-card > .ant-tabs-bar { 25 | border-color: #fff; 26 | } 27 | 28 | .ant-tabs-card > .ant-tabs-bar .ant-tabs-tab { 29 | border-color: transparent; 30 | background: transparent; 31 | } 32 | 33 | .ant-tabs-card > .ant-tabs-bar .ant-tabs-tab-active { 34 | border-color: #fff; 35 | background: #fff; 36 | } 37 | 38 | .ant-progress { 39 | padding-top: 10px; 40 | } 41 | 42 | .ant-card-head { 43 | background: #F3F3F3; 44 | } 45 | ` 46 | 47 | const PageHeader = styled.h2` 48 | padding-bottom: 30px; 49 | ` 50 | 51 | export default function Analysis({ isMobile, isLoading, data }) { 52 | 53 | const perf = data.performance || {"funding":5.2204930393039,"fundingCat":100,"alexa":52.23,"alexaCat":52.22,"twitter":3.0,"twitterCat":100,"employees":11.2,"employeesCat":42.0,"growth":22.1,"growthCat":82.44}; 54 | 55 | const onCardSelect = (e,props) => { 56 | const target = props.link || "exit" 57 | console.log("Select Card: ", target) 58 | Router.push(`/ranking?id=${target}`,`/ranking/${target}`) 59 | } 60 | 61 | const RankingBox = props => ( 62 | } 66 | onClick={(e) => { onCardSelect(e,props) } } 67 | > 68 | {props.info} 69 | 70 | 71 | ) 72 | 73 | const pt = number => number != null ? (number + 0.049999999).toFixed(1) : 0.0 // Round up to 0.1 74 | 75 | return ( 76 | 77 | Company Analysis 78 | 83 | Performance vs. Cohort} key="1"> 84 | 85 | 86 | Funding 87 | Exit 88 | 89 |
90 | 91 | Employees 92 | Alexa 93 | 94 | 95 | 96 | 97 | vs. Sector} key="2"> 98 | 99 | 100 | Funding 101 | Exit 102 | 103 |
104 | 105 | Employees 106 | Alexa 107 | 108 | 109 | 110 | 111 | 112 | 113 | ); 114 | } 115 | Analysis.propTypes = { 116 | isMobile: PropTypes.bool, 117 | isLoading: PropTypes.bool, 118 | data: PropTypes.object, 119 | }; -------------------------------------------------------------------------------- /src/app/components/CompanyDetails.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Router from "next/router"; 4 | import { Card, Row, Col, Avatar, Button, Icon } from 'antd'; 5 | import FirebaseProvider from '../lib/FirebaseProvider'; 6 | import CompanyAnalysis from './CompanyAnalysis'; 7 | import CompanyFeedback from './Feedback'; 8 | import Masthead from './Masthead'; 9 | import CompanyContact from './Contact'; 10 | import styled from 'styled-components'; 11 | 12 | const Page1Container = styled.div` 13 | padding-top: 30px; 14 | padding-bottom: 30px; 15 | background-color: #f8f9fa; 16 | ` 17 | const PageHeader = styled.h1` 18 | text-align: center; 19 | padding-bottom: 30px; 20 | ` 21 | 22 | 23 | export default function CompanyDetails({ isMobile, companyID }) { 24 | 25 | const { Meta } = Card; 26 | 27 | const onCardSelect = (e,props) => { 28 | const target = props.link || "exit" 29 | console.log("Select Card: ", target) 30 | // Router.push(`/ranking?id=${target}`,`/ranking/${target}`) 31 | } 32 | 33 | 34 | return ( 35 | 36 | 37 | { ({error, isLoading, data}) => { 38 | 39 | if (error) { console.error("Error loading company ", error)} 40 | 41 | // console.log(`CompanyDetails: [${companyID}] ${JSON.stringify(data)}`) 42 | 43 | const co = data[0] || {} 44 | const name = isLoading ? 'Loading Company...' : co.name 45 | 46 | return( 47 | 48 | {} 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | ) 61 | }} 62 | 63 | 64 | 65 | ); 66 | } 67 | CompanyDetails.propTypes = { 68 | isMobile: PropTypes.bool, 69 | companyID: PropTypes.string, 70 | }; -------------------------------------------------------------------------------- /src/app/components/Contact.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Router from "next/router"; 4 | import { Skeleton, Button } from 'antd'; 5 | import styled from 'styled-components'; 6 | import dynamic from 'next/dynamic' 7 | const Map = dynamic(() => import('./Map.js'), { 8 | ssr: false 9 | }); 10 | 11 | const ContactSection = styled.div` 12 | text-align: left; 13 | border-left: 1px solid #d9d9d9; 14 | padding-left: 40px; 15 | ` 16 | 17 | const MapBox = props =>

{props.title}

{props.children}
; 18 | 19 | export default function CompanyContact({ isMobile, isLoading, data }) { 20 | 21 | const www = isLoading ? 'https://jpvcdb.co' : data.www 22 | const address = isLoading ? :

{data.address}

23 | const city = isLoading ? :

{data.city}

24 | const zip = isLoading ? :

{data.zip}

25 | const twitter = data.twitterID || 'https://twitter.com/jpvcdb' 26 | const facebook = data.facebookID || 'https://facebook.com/' 27 | const coord = data.coordinates || { lng: 139.7454, lat: 35.6586 } 28 | 29 | return ( 30 | 31 | 32 |
33 |
34 |

Social

35 |
36 |
39 | 40 |
41 |
42 |

Address

43 |
44 |
{address}
45 |
{city}
46 |
{zip}
47 |
48 | 49 |
50 |
51 |

Location

52 |
53 | 54 |
55 |
56 | ); 57 | } 58 | CompanyContact.propTypes = { 59 | isMobile: PropTypes.bool, 60 | isLoading: PropTypes.bool, 61 | data: PropTypes.object, 62 | }; -------------------------------------------------------------------------------- /src/app/components/Feedback.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Router from "next/router"; 4 | import { Row, Col, Icon, Form, Button, Input, Radio, Rate, notification } from 'antd'; 5 | import styled from 'styled-components'; 6 | import createMessage from '../lib/createMessage' 7 | 8 | const FormItem = Form.Item; 9 | const RadioButton = Radio.Button; 10 | const RadioGroup = Radio.Group; 11 | const { TextArea } = Input; 12 | 13 | const FeedbackContainer = styled.div` 14 | padding: 30px; 15 | background: #fbfbfb; 16 | border: 1px solid #d9d9d9; 17 | border-radius: 6px; 18 | ` 19 | 20 | const PageHeader = styled.h2` 21 | padding-bottom: 30px; 22 | ` 23 | 24 | class MessageCreate extends React.Component { 25 | state = { 26 | id: this.props.id || 'unknown', 27 | deploying: false, 28 | submitVisible: true, 29 | loading: false 30 | } 31 | 32 | handleOk = () => { 33 | this.setState({ 34 | submitVisible: false, 35 | }); 36 | } 37 | 38 | handleSubmit = (e) => { 39 | e.preventDefault(); 40 | const {history} = this.props; 41 | this.props.form.validateFields((err, values) => { 42 | if (!err) { 43 | console.log("title:", values.title) 44 | console.log("body:", values.body) 45 | 46 | let message = { company: this.state.id, 47 | name: values.name, 48 | email: values.email, 49 | message: values.message, 50 | problem: values.problem, 51 | rating: values.rating} 52 | 53 | createMessage(message) 54 | .then( (resp) => { 55 | 56 | if (resp) { 57 | notification.info({ 58 | message: "Message Sent!", 59 | description: `Thank you ${values.name}. We recieved your message.` 60 | }) 61 | 62 | this.props.form.resetFields() 63 | 64 | this.setState({ 65 | submitVisible: false 66 | }); 67 | 68 | } 69 | }) 70 | 71 | } 72 | else { 73 | var errorCode = err.code || 'Sorry, there was a problem.'; 74 | var errorMessage = err.message || 'Please correct the errors and submit again' 75 | 76 | notification.error({ 77 | message: errorCode, 78 | description: errorMessage 79 | }) 80 | } 81 | }) 82 | } 83 | 84 | render() { 85 | const { getFieldDecorator } = this.props.form; 86 | const { deploying, loading } = this.state; 87 | 88 | return ( 89 |
90 | 91 |
92 | 93 | 94 | {getFieldDecorator('name', { 95 | rules: [{ required: true, message: 'Please tell us who you are.' }], 96 | })( 97 | } placeholder=" Your Name" /> 98 | )} 99 | 100 | 101 | 102 | {getFieldDecorator('email', { 103 | rules: [{type: 'email', message: 'The input is not valid E-mail!'}, 104 | { required: true, message: 'Please tell us how to contact you.' }], 105 | })( 106 | } placeholder=" Your Email" /> 107 | )} 108 | 109 | 110 | 111 | {getFieldDecorator('problem', { 112 | rules: [{ required: true, message: 'Please explain the problem' }], 113 | })( 114 | 115 | Bug 116 | Bad Data 117 | Question 118 | 119 | )} 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | {getFieldDecorator('message', { 128 | rules: [{ required: true, message: 'Please input your message!' }], 129 | })( 130 |