mirror of https://github.com/cncf/landscapeapp.git
Compare commits
262 Commits
Author | SHA1 | Date |
---|---|---|
|
a882d0675b | |
|
5ffa213fc9 | |
|
2344353df5 | |
|
b7e063d94e | |
|
e915165065 | |
|
3de416fa3a | |
|
7dd727758c | |
|
5235155c90 | |
|
d8e4f84e5c | |
|
61014620c2 | |
|
8137985380 | |
|
8c4b4941de | |
|
b2c7d52080 | |
|
5e05834a46 | |
|
450ca5e141 | |
|
d524f88001 | |
|
12667a2b52 | |
|
52f2254d91 | |
|
10155fd1ea | |
|
bc541000c9 | |
|
b3638c04aa | |
|
bd563a8465 | |
|
485cb1158d | |
|
1a3e59e157 | |
|
b784d7f896 | |
|
b45ce81784 | |
|
808d219202 | |
|
2bda551a74 | |
|
3775e220a3 | |
|
0b41bc54da | |
|
97e8e882a3 | |
|
62fa27892c | |
|
8ce89db6e3 | |
|
0b66094876 | |
|
d4409f142a | |
|
28973ed0bc | |
|
7a983a6583 | |
|
af91ca7e16 | |
|
d4850f64fd | |
|
af77ba322a | |
|
73ac6d4efe | |
|
ffbb154c51 | |
|
90d9bead25 | |
|
a17dc8d542 | |
|
0b0314f636 | |
|
d8c453c471 | |
|
b85dbd9100 | |
|
3a01088cda | |
|
b0f6c86335 | |
|
06d592b3ba | |
|
60ea7c8551 | |
|
f9b1fbe8ab | |
|
d9020382c6 | |
|
1c5e8aaee3 | |
|
5b8254f1b2 | |
|
e12aaca02f | |
|
2775c25b5b | |
|
4ffbff6f9c | |
|
1ac6a2c97f | |
|
9aa1006f60 | |
|
b5802a5545 | |
|
fe9351f824 | |
|
8434098eaf | |
|
8dc5fd6c82 | |
|
f922f3831d | |
|
fd3fc93889 | |
|
4723c94b56 | |
|
97dd8d8f8b | |
|
aebf1d588e | |
|
1eb67b3668 | |
|
a12ce99e58 | |
|
862a33eacd | |
|
495c7997e8 | |
|
bd8f8f8d03 | |
|
cc125f24dd | |
|
250bbe68b8 | |
|
f987c05cb7 | |
|
e577cbd700 | |
|
2ae215c8c2 | |
|
901182882c | |
|
a657ae8540 | |
|
628c6f71a5 | |
|
60e5c99514 | |
|
2dc0d29598 | |
|
d330c59d41 | |
|
e0429f758d | |
|
95e60ef4a9 | |
|
9304e3a777 | |
|
c5b75d14e2 | |
|
899dca8561 | |
|
b762217ab1 | |
|
8a8003b41f | |
|
c99ae2514a | |
|
fdab31e0f7 | |
|
e449569b86 | |
|
d6c2ecad33 | |
|
ad5d51afe7 | |
|
a49ff4b891 | |
|
5a572110fe | |
|
f6dbef23f3 | |
|
461c9d6d8a | |
|
dae74b9d6d | |
|
21d8621271 | |
|
60d7704d7d | |
|
526822323c | |
|
47a6b6fb00 | |
|
17f088a0de | |
|
f6cd147f77 | |
|
1088b323ae | |
|
8c75b38bd2 | |
|
e347630c9e | |
|
674dd36513 | |
|
2ba49cc7a6 | |
|
1ab80e01c1 | |
|
c5e8289c77 | |
|
1c6df12abf | |
|
5e5110ca52 | |
|
875014bac0 | |
|
05723ec72f | |
|
68452b5dfd | |
|
53d01e8982 | |
|
0d0004ae18 | |
|
921e8a3cdf | |
|
544ba79b1d | |
|
645e77ea29 | |
|
8cd5ba7a86 | |
|
12de9ea4f8 | |
|
848670d1e6 | |
|
b03a7878f2 | |
|
c71dea1f6e | |
|
14c675ecf5 | |
|
0889f7833a | |
|
6b234663e9 | |
|
2b0075056a | |
|
fba9b1c416 | |
|
d338c0bb39 | |
|
fa34affd2d | |
|
1437c53605 | |
|
04ef289bae | |
|
90daf28206 | |
|
cb1b161a76 | |
|
8a281f8be2 | |
|
a59ccb133f | |
|
7b4580f15d | |
|
b1748d9ad8 | |
|
8316ec3a9c | |
|
84ee4cae5d | |
|
8728f7a67d | |
|
34a2801c12 | |
|
4274e2dce0 | |
|
35e442b220 | |
|
9dd4cbbce7 | |
|
f420554102 | |
|
51b07fd90f | |
|
5e8272e951 | |
|
dc9ab23e3c | |
|
944b9d4ca5 | |
|
1d976569e6 | |
|
4afbf066e1 | |
|
1311cc87b3 | |
|
b36a6dde0f | |
|
d30b8ad818 | |
|
5a4955b941 | |
|
ae5ce3d9b0 | |
|
6bd23218e7 | |
|
f0fe09fbf7 | |
|
ca1d9882af | |
|
e286ddaef9 | |
|
b4f92d7bb9 | |
|
fab5d5ab34 | |
|
3eb7b5d0f0 | |
|
5b9f12d80d | |
|
a292620f7a | |
|
7a51987a03 | |
|
282460a37a | |
|
84749ce870 | |
|
ebb53fe075 | |
|
c72a1e6148 | |
|
1d9f8c31d6 | |
|
745e5057a4 | |
|
2eeafd345d | |
|
abc521cefc | |
|
1aa6cf747e | |
|
d2719d926b | |
|
03321ab164 | |
|
54a7575b94 | |
|
e8e823e25b | |
|
e8a6db4982 | |
|
c1c2fb26d1 | |
|
9009ceb9ad | |
|
991798616a | |
|
4c5e1df907 | |
|
11dba210df | |
|
e4e706e992 | |
|
3cd5090092 | |
|
dea1363831 | |
|
b0a6aa9ef8 | |
|
619f7b7847 | |
|
7e645dbc3e | |
|
6dba098fcc | |
|
0793fafd05 | |
|
1f5285428e | |
|
9adb65ee38 | |
|
892362ea1f | |
|
c080c125aa | |
|
550d8f30d7 | |
|
a79556b99e | |
|
dec187b4e5 | |
|
52efed17d6 | |
|
611cf5f654 | |
|
20f32d35d0 | |
|
54fade6a94 | |
|
1f7c082cca | |
|
0d1e0e77e6 | |
|
853af61ab4 | |
|
480aea9d04 | |
|
67a3b089de | |
|
27f62a94ae | |
|
b21c5fd597 | |
|
926351f335 | |
|
409f06a363 | |
|
87f8445b71 | |
|
cc71725661 | |
|
e793eb733b | |
|
3eb689d578 | |
|
ee27648dfc | |
|
bed59adea3 | |
|
3178e88459 | |
|
7babd5b764 | |
|
0d7155766b | |
|
adff0ebd1b | |
|
86e23443e4 | |
|
ae6b3f006e | |
|
c484efebba | |
|
ce1d6dbdfc | |
|
dafa28aa1e | |
|
9065ea761a | |
|
ab0c184625 | |
|
403a5651e0 | |
|
a4e97097f8 | |
|
2f058973f0 | |
|
6bed68ba99 | |
|
9d20d4fe1a | |
|
6d9843cddf | |
|
e9a3448952 | |
|
5dcf4a478a | |
|
cdbce7aa84 | |
|
eb7d99d9ba | |
|
9b5c3c6ffb | |
|
71b54d5a62 | |
|
fe515ca7d2 | |
|
97f425b465 | |
|
c7e219b52c | |
|
9a150a523c | |
|
0588a0a63c | |
|
3d8522ba7e | |
|
dfce515043 | |
|
e180f8c6e5 | |
|
03cb35fad4 | |
|
fe5fdec626 | |
|
624413a037 | |
|
6bac7f4231 |
16
.babelrc.js
16
.babelrc.js
|
@ -1,16 +0,0 @@
|
|||
const path = require('path')
|
||||
|
||||
// CAREFUL before adding more presets, next/babel already includes some
|
||||
// see https://nextjs.org/docs/advanced-features/customizing-babel-config
|
||||
module.exports = {
|
||||
ignore: [".yarn", ".pnp.js"],
|
||||
presets: ["next/babel"],
|
||||
plugins: [
|
||||
["module-resolver", {
|
||||
alias: {
|
||||
public: path.resolve(__dirname, 'public'),
|
||||
project: process.env.PROJECT_PATH
|
||||
}
|
||||
}]
|
||||
]
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
netlify/jsyaml.js
|
|
@ -0,0 +1,19 @@
|
|||
module.exports = {
|
||||
"extends": "eslint:recommended",
|
||||
"root": true,
|
||||
"env": {
|
||||
"browser": true,
|
||||
"commonjs": true,
|
||||
"es2021": true,
|
||||
"node": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest"
|
||||
},
|
||||
"rules": {
|
||||
"no-useless-escape": 0,
|
||||
"no-prototype-builtins": 0,
|
||||
"no-empty": 0,
|
||||
"no-control-regex": 0
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,3 +1,3 @@
|
|||
enableProgressBars: false
|
||||
yarnPath: .yarn/releases/yarn-sources.cjs
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-3.2.1.cjs
|
||||
|
|
38
README.md
38
README.md
|
@ -26,7 +26,7 @@
|
|||
- [Generating a Guide](#generating-a-guide)
|
||||
|
||||
|
||||
The landscapeapp is an upstream NPM [module](https://www.npmjs.com/package/interactive-landscape) that supports building interactive landscape websites such as the [CNCF Cloud Native Landscape](https://landscape.cncf.io) ([source](https://github.com/cncf/landscape)) and the [LF Artificial Intelligence Landscape](https://landscape.lfai.foundation) ([source](https://github.com/lfai/lfai-landscape)). The application is managed by [Dan Kohn](https://www.dankohn.com) of [CNCF](https://www.cncf.io) and is under active development by [Andrey Kozlov](https://github.com/ZeusTheTrueGod) (who did most of the development to date) and [Jordi Noguera](https://jordinl.com).
|
||||
The landscapeapp is an upstream NPM [module](https://www.npmjs.com/package/interactive-landscape) that supports building interactive landscape websites such as the [CNCF Cloud Native Landscape](https://landscape.cncf.io) ([source](https://github.com/cncf/landscape)) and the [LF Artificial Intelligence Landscape](https://landscape.lfai.foundation) ([source](https://github.com/lfai/lfai-landscape)). The application is under active development by [Andrey Kozlov](https://github.com/AndreyKozlov1984) and [Jordi Noguera](https://jordinl.com).
|
||||
|
||||
In addition to creating fully interactive sites, the landscapeapp builds static images on each update. See examples in [ADOPTERS.md](ADOPTERS.md). All current [Linux Foundation](https://linuxfoundation.org) landscapes are listed in [landscapes.yml](landscapes.yml).
|
||||
|
||||
|
@ -55,7 +55,7 @@ Additional keys that can be set are defined below:
|
|||
project_org:
|
||||
# additional repos for the project; will fetch stats if they start with https://github.com/
|
||||
additional_repos:
|
||||
# Stock Ticker for the organization of the project/entry; normally pulls from Crunchbase but can be overriden here. For delisted and many foreign countries, you'll need to add `stock_ticker` with the value to look up on Yahoo Finance to find the market cap.
|
||||
# Stock Ticker for the organization of the project/entry; normally pulls from Crunchbase but can be overridden here. For delisted and many foreign countries, you'll need to add `stock_ticker` with the value to look up on Yahoo Finance to find the market cap.
|
||||
stock_ticker:
|
||||
# description of the entry; if not set pulls from the GitHub repo description
|
||||
description:
|
||||
|
@ -67,7 +67,7 @@ Additional keys that can be set are defined below:
|
|||
url_for_bestpractices:
|
||||
# set to false if a repo_url is given but the entry is a project that isn't open source
|
||||
open_source:
|
||||
# allows mulitple entries with the same repo_url; set for each instance
|
||||
# allows multiple entries with the same repo_url; set for each instance
|
||||
allow_duplicate_repo:
|
||||
# set to true if you are using an anonymous organization. You will also need anonymous_organization set in settings.yml
|
||||
unnamed_organization:
|
||||
|
@ -77,7 +77,7 @@ For some of the key, there is some guidance as listed below.
|
|||
|
||||
### Logos
|
||||
|
||||
The most challenging part of creating a new landscape is finding SVG images for all projects and companies. These landscapes represent a valuable resource to a community in assembling all related projects, creating a taxonomy, and providing the up-to-date logos, and unfortunately, there are no shortcuts.
|
||||
The most challenging part of creating a new landscape is finding SVG images for all projects and companies. These landscapes represent a valuable resource to a community in assembling all related projects, creating a taxonomy, and providing up-to-date logos, and unfortunately, there are no shortcuts.
|
||||
|
||||
Do *not* try to convert PNGs to SVGs. You can't automatically go from a low-res to a high-res format, and you'll just waste time and come up with a substandard result. Instead, invest your time finding SVGs and then (when necessary) having a graphic designer recreate images when high res ones are not available.
|
||||
|
||||
|
@ -92,7 +92,7 @@ For new landscapes of any size, you will probably need a graphic artist to rebui
|
|||
|
||||
If the project is hosted/sponsored by an organization but doesn't have a logo, best practice is to use that organization's logo with the title of the project underneath ( [example](https://landscape.cncf.io/selected=netflix-eureka) ). You can use a tool such as [Inkscape](https://inkscape.org/) to add the text.
|
||||
|
||||
If you get an error with the image that it has a PNG embeded, you will need to find a different SVG that doesn't include a PNG or work with a graphic artist to rebuild the logo.
|
||||
If you get an error with the image that it has a PNG embedded, you will need to find a different SVG that doesn't include a PNG or work with a graphic artist to rebuild the logo.
|
||||
|
||||
#### SVGs Can't Include Text
|
||||
|
||||
|
@ -128,6 +128,8 @@ We require all landscape entries to include a [Crunchbase](https://www.crunchbas
|
|||
|
||||
Using an external source for this info saves effort in most cases, because most organizations are already listed. Going forward, the data is being independently maintained and updated over time.
|
||||
|
||||
If for certain reason Crunchbase should not be used - we rely on `organization: { name: 'My Organization Name' }` instead of a `crunchbase` field
|
||||
|
||||
#### Overriding industries from Crunchbase
|
||||
|
||||
To override industries returned from Crunchbase for a specific Crunchbase entry, add it to an `crunchbase_overrides` top-level entry on `landscape.yml`. For instance, the following will set `industries` for Linux Foundation to Linux and Cloud Computing:
|
||||
|
@ -158,7 +160,7 @@ The update server enhances the source data with the fetched data and saves the r
|
|||
If you want to create an interactive landscape for your project or organization:
|
||||
1. Note ahead of time that the hardest part of building a landscape is getting hi-res images for every project. You *cannot* convert from a PNG or JPEG into an SVG. You need to get an SVG, AI, or EPS file. When those aren't available, you will need a graphic designer to recreate several images. Don't just use an auto-tracer to try to convert PNG to SVG because there is some artistry involved in making it look good. Please review this [primer](https://www.cncf.io/blog/2019/07/17/what-image-formats-should-you-be-using-in-2019/) on image formats.
|
||||
2. Create a repo `youracronym-landscape` so it's distinct from other landscapes stored in the same directory. From inside your new directory, copy over files from a simpler landscape like https://github.com/graphql/graphql-landscape with `cp -r ../graphql-landscape/* ../graphql-landscape/.github ../graphql-landscape/.gitignore ../graphql-landscape/.npmrc ../graphql-landscape/.nvmrc .`.
|
||||
3. If you're working with the [LF](https://www.linuxfoundation.org/), give admin privileges to the new repo to [dankohn](https://github.com/dankohn) and write privleges to [AndreyKozlov1984](https://github.com/AndreyKozlov1984), [jordinl83](https://github.com/jordinl83), and [CNCF-Bot](https://github.com/CNCF-Bot) and ping Dan after creating an account at [slack.cncf.io](https://slack.cncf.io). Alex Contini and Dan are available there to help you recreate SVGs based on a PNG of the company's logo, if necessary, and to fix other problems.
|
||||
3. If you're working with the [LF](https://www.linuxfoundation.org/), give admin privileges to the new repo to [dankohn](https://github.com/dankohn) and write privileges to [AndreyKozlov1984](https://github.com/AndreyKozlov1984), [jordinl83](https://github.com/jordinl83), and [CNCF-Bot](https://github.com/CNCF-Bot) and ping Dan after creating an account at [slack.cncf.io](https://slack.cncf.io). Alex Contini and Dan are available there to help you recreate SVGs based on a PNG of the company's logo, if necessary, and to fix other problems.
|
||||
4. Set the repo to only support merge commits and turn off DCO support, since it doesn't work well with the GitHub web interface:
|
||||

|
||||
5. Edit `settings.yml` and `landscape.yml` for your topic.
|
||||
|
@ -168,7 +170,7 @@ If you want to create an interactive landscape for your project or organization:
|
|||
|
||||
### API Keys
|
||||
|
||||
You want to add the following to your `~/.bash_profile`. If you're with the LF, ask Dan Kohn on CNCF [Slack](https://slack.cncf.io) for the Crunchbase and Twitter keys.
|
||||
You want to add the following to your `~/.bash_profile`. If you're with the LF, ask someone on CNCF [Slack](https://slack.cncf.io) for the Crunchbase and Twitter keys.
|
||||
|
||||
For the GitHub key, please go to https://github.com/settings/tokens and create a key (you can call it `personal landscape`) with *no* permissions. That is, don't click any checkboxes, because you only need to access public repos.
|
||||
|
||||
|
@ -190,22 +192,6 @@ dev$ cd landscapeapp
|
|||
dev$ npm install -g yarn@latest
|
||||
dev$ yarn
|
||||
```
|
||||
Now, to use the local landscapeapp you can add the following to your `~/.bash_profile` or `.zshrc`:
|
||||
```sh
|
||||
function y { export PROJECT_PATH=`pwd` && (cd ../landscapeapp && yarn run "$@")}
|
||||
export -f y
|
||||
# yf does a normal build and full test run
|
||||
alias yf='y fetch'
|
||||
alias yl='y check-links'
|
||||
alias yq='y remove-quotes'
|
||||
# yp does a build and then opens up the landscape in your browser ( can view the PDF and PNG files )
|
||||
alias yp='y build && y open:dist'
|
||||
# yo does a quick build and opens up the landscape in your browser
|
||||
alias yo='y open:src'
|
||||
alias a='for lpath in /Users/your-username/dev/{landscapeapp,cdf-landscape,lfai-landscape}; do echo $lpath; git -C $lpath pull -p; done; (cd /Users/your-username/dev/landscapeapp && yarn);'
|
||||
|
||||
```
|
||||
Reload with `. ~/.bash_profile` and then use `yo`, `yf`, etc. to run functions on the landscape in your landscape directory. `a` will do a git pull on each of the project directories you specify and install any necessary node modules for landscapeapp.
|
||||
|
||||
### Adding to a google search console
|
||||
Go to the google search console, add a new property, enter the url of the
|
||||
|
@ -215,8 +201,8 @@ Reload with `. ~/.bash_profile` and then use `yo`, `yf`, etc. to run functions o
|
|||
an `html tag verification` option and copy a secret code from it and put it to
|
||||
the `settings.yml` of a given landscape project. Then commit the change to the default branch and
|
||||
wait till Netlify deploys the default branch. The key is named `google_site_veryfication` and it is
|
||||
somewhere around line 14 in settings.yml. After netlify succesfully deploys
|
||||
that dashbaord, verify the html tag in a google console. Do not forget to add
|
||||
somewhere around line 14 in settings.yml. After netlify successfully deploys
|
||||
that dashboard, verify the html tag in a google console. Do not forget to add
|
||||
Dan@linuxfoundation.org as someone who has a full access from a `Settings`
|
||||
menu for a given search console.
|
||||
|
||||
|
@ -391,4 +377,4 @@ Don't include a title for the section, level 3 heading will be automatically gen
|
|||
|
||||
### Automatic generation of guide navigation
|
||||
|
||||
The guide will include a a side-navigation generated automatically from all the headings levels 2 and 3 found on the guide. Level 3 headings will be nested under the closest level 2 heading above.
|
||||
The guide will include a side-navigation generated automatically from all the headings levels 2 and 3 found on the guide. Level 3 headings will be nested under the closest level 2 heading above.
|
||||
|
|
9
_headers
9
_headers
|
@ -2,15 +2,6 @@
|
|||
X-Robots-Tag: all
|
||||
Access-Control-Allow-Origin: *
|
||||
|
||||
/*.js
|
||||
Cache-Control: public, max-age=31536000, immutable
|
||||
|
||||
/*.css
|
||||
Cache-Control: public, max-age=31536000, immutable
|
||||
|
||||
/*.woff2
|
||||
Cache-Control: public, max-age=31536000, immutable
|
||||
|
||||
/*.svg
|
||||
Content-Type: image/svg+xml; charset=utf-8
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
1
dyf.sh
1
dyf.sh
|
@ -1 +0,0 @@
|
|||
docker run -e CRUNCHBASE_KEY_4=$CRUNCHBASE_KEY_4 -e TWITTER_KEYS=$TWITTER_KEYS -e GITHUB_KEY=$GITHUB_KEY -it --user root --rm -v $(pwd)/../landscapeapp:/landscapeapp -v $(pwd):/repo -w /usr/app netlify/build:xenial /bin/bash -c '. /opt/buildhome/.nvm/install.sh; . /root/.nvm/nvm.sh; cd /landscapeapp; nvm install `cat .nvmrc`; nvm use `cat .nvmrc`; npm install -g yarn; yarn; PROJECT_PATH=/repo yarn fetch'
|
1
dyo.sh
1
dyo.sh
|
@ -1 +0,0 @@
|
|||
docker run -p 3000:3000 -e CRUNCHBASE_KEY_4=$CRUNCHBASE_KEY_4 -e TWITTER_KEYS=$TWITTER_KEYS -e GITHUB_KEY=$GITHUB_KEY -it --user root --rm -v $(pwd)/../landscapeapp:/landscapeapp -v $(pwd):/repo -w /usr/app netlify/build:xenial /bin/bash -c 'echo $CRUNCHBASE_KEY_4; . /opt/buildhome/.nvm/install.sh; . /root/.nvm/nvm.sh; cd /landscapeapp; nvm install `cat .nvmrc`; nvm use `cat .nvmrc`; npm install -g yarn; yarn; PROJECT_PATH=/repo yarn open:src'
|
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
"verbose": true
|
||||
}
|
|
@ -16,4 +16,4 @@ nvm use
|
|||
npm install -g npm
|
||||
npm install -g yarn
|
||||
yarn
|
||||
yarn run babel-node tools/landscapes.js
|
||||
yarn node tools/landscapes.js
|
||||
|
|
|
@ -1,19 +1,30 @@
|
|||
ip: 145.40.64.211
|
||||
ip: 147.75.72.237
|
||||
landscapes:
|
||||
# name: how we name a landscape project, used on a build server for logs and settings
|
||||
# repo: a github repo for a specific landscape
|
||||
# netlify: full | skip - do we build it on a netlify build or not
|
||||
# hook: - id for a build hook, so it will be triggered after a default branch build
|
||||
- landscape:
|
||||
name: lfedge
|
||||
repo: State-of-the-Edge/lfedge-landscape
|
||||
hook: 5c80e31894c5c7758edb31e4
|
||||
- landscape:
|
||||
name: lfenergy
|
||||
repo: lf-energy/lfenergy-landscape
|
||||
hook: 606487bb9da603110d4b8139
|
||||
- landscape:
|
||||
name: lf
|
||||
repo: jmertic/lf-landscape
|
||||
hook: 606487123224e20fb8f3896e
|
||||
- landscape:
|
||||
name: dlt
|
||||
repo: dltlandscape/dlt-landscape
|
||||
hook: demo
|
||||
- landscape:
|
||||
name: aswf-landscape
|
||||
repo: AcademySoftwareFoundation/aswf-landscape
|
||||
hook: 608aa68eb6e5723d5d8a7e00
|
||||
required: true
|
||||
- landscape:
|
||||
name: cncf
|
||||
repo: cncf/landscape
|
||||
hook: 5c1bd968fdd72a78a54bdcd1
|
||||
required: true
|
||||
- landscape:
|
||||
name: cdf
|
||||
repo: cdfoundation/cdf-landscape
|
||||
|
@ -32,23 +43,11 @@ landscapes:
|
|||
repo: graphql/graphql-landscape
|
||||
hook: 5d5c7ccf64ecb5bd3d2592f7
|
||||
required: true
|
||||
- landscape:
|
||||
name: lf
|
||||
repo: jmertic/lf-landscape
|
||||
hook: 606487123224e20fb8f3896e
|
||||
- landscape:
|
||||
name: lfai
|
||||
repo: lfai/landscape
|
||||
hook: 60648e5b74c76017210a2f53
|
||||
required: true
|
||||
- landscape:
|
||||
name: lfedge
|
||||
repo: State-of-the-Edge/lfedge-landscape
|
||||
hook: 5c80e31894c5c7758edb31e4
|
||||
- landscape:
|
||||
name: lfenergy
|
||||
repo: lf-energy/lfenergy-landscape
|
||||
hook: 606487bb9da603110d4b8139
|
||||
- landscape:
|
||||
name: lfph
|
||||
repo: lfph/lfph-landscape
|
||||
|
@ -62,10 +61,6 @@ landscapes:
|
|||
name: openssf
|
||||
repo: ossf/ossf-landscape
|
||||
hook: 613768338a16cb9182b21c3d
|
||||
- landscape:
|
||||
name: ospo
|
||||
repo: todogroup/ospolandscape
|
||||
hook: 6033b43a2572da242aee6a92
|
||||
- landscape:
|
||||
name: presto
|
||||
repo: prestodb/presto-landscape
|
||||
|
@ -78,3 +73,11 @@ landscapes:
|
|||
name: ucf
|
||||
repo: ucfoundation/ucf-landscape
|
||||
hook: 5d96662e28b476477790dd8a
|
||||
- landscape:
|
||||
name: lfn
|
||||
repo: lfnetworking/member_landscape
|
||||
hook: 60788f5fa3cbd68181e3c209
|
||||
- landscape:
|
||||
name: riscv
|
||||
repo: riscv-admin/riscv-landscape
|
||||
hook: demo
|
||||
|
|
|
@ -9,3 +9,5 @@ server via rsync.
|
|||
|
||||
Most chances is that we will switch to a different build tool soon, so this is
|
||||
an experimental approach to speedup netlify builds.
|
||||
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,19 +1,10 @@
|
|||
// We will execute this script from a landscape build,
|
||||
// "prepublish": "cp yarn.lock _yarn.lock",
|
||||
// "postpublish": "rm _yarn.lock || true"
|
||||
const LANDSCAPEAPP = process.env.LANDSCAPEAPP || "latest"
|
||||
const remote = `root@${process.env.BUILD_SERVER}`;
|
||||
const dockerImage = 'netlify/build:xenial';
|
||||
const dockerImage = 'netlify/build:focal';
|
||||
const dockerHome = '/opt/buildhome';
|
||||
|
||||
const systemName = require('child_process').execSync('lsb_release -a').toString();
|
||||
const is1604 = systemName.indexOf('16.04');
|
||||
console.info(systemName, is1604);
|
||||
if (!is1604) {
|
||||
console.info('Please ensure that you have a 16.04 ubuntu image for this netlify project, current lsb_release -a', systemName);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const secrets = [
|
||||
process.env.CRUNCHBASE_KEY_4, process.env.TWITTER_KEYS, process.env.GITHUB_TOKEN, process.env.GITHUB_USER, process.env.GITHUB_KEY
|
||||
].filter( (x) => !!x);
|
||||
|
@ -39,27 +30,20 @@ const debug = function() {
|
|||
}
|
||||
}
|
||||
|
||||
const runLocal = function(command, options = {}) {
|
||||
const { assignFn, showOutputFn } = options;
|
||||
const runLocal = function(command, showProgress) {
|
||||
|
||||
// report the output once every 5 seconds
|
||||
let lastOutput = { s: '', time: new Date().getTime() };
|
||||
let displayIfRequired = function(text) {
|
||||
lastOutput.s = lastOutput.s + text;
|
||||
if (showOutputFn && showOutputFn()) {
|
||||
if (lastOutput.done || new Date().getTime() > lastOutput.time + 5 * 1000) {
|
||||
console.info(lastOutput.s);
|
||||
lastOutput.s = "";
|
||||
lastOutput.time = new Date().getTime();
|
||||
};
|
||||
if (showProgress) {
|
||||
console.info(text);
|
||||
}
|
||||
lastOutput.s = lastOutput.s + text;
|
||||
}
|
||||
|
||||
return new Promise(function(resolve) {
|
||||
var spawn = require('child_process').spawn;
|
||||
var child = spawn('bash', ['-lc',`set -e \n${command}`]);
|
||||
if (assignFn) {
|
||||
assignFn(child);
|
||||
}
|
||||
let output = [];
|
||||
child.stdout.on('data', function(data) {
|
||||
const text = maskSecrets(data.toString('utf-8'));
|
||||
|
@ -92,72 +76,6 @@ const runLocalWithoutErrors = async function(command) {
|
|||
return result.text.trim();
|
||||
}
|
||||
|
||||
let buildDone = false;
|
||||
let localPid;
|
||||
let remoteFailed = false;
|
||||
let localFailed = false;
|
||||
|
||||
async function getPids() {
|
||||
const result = await runLocalWithoutErrors(`ps`);
|
||||
const lines = result.split('\n').map( (x) => x.trim()).filter( (x) => x).slice(1);
|
||||
const pids = lines.map( (line) => line.split(' ')[0]);
|
||||
console.info('pids:', pids);
|
||||
return pids;
|
||||
}
|
||||
|
||||
let initialPids;
|
||||
|
||||
const makeLocalBuild = async function() {
|
||||
const localOutput = await runLocal(`
|
||||
# mkdir -p copy
|
||||
# rsync -az --exclude="copy" . copy
|
||||
# cd copy
|
||||
. ~/.nvm/nvm.sh
|
||||
npm pack interactive-landscape@${LANDSCAPEAPP}
|
||||
tar xzf interactive*
|
||||
cd package
|
||||
cp _yarn.lock yarn.lock
|
||||
echo 0
|
||||
nvm install
|
||||
echo 1
|
||||
nvm use
|
||||
echo 2
|
||||
npm install -g agentkeepalive --save
|
||||
echo 3
|
||||
npm install -g npm --no-progress
|
||||
echo 4
|
||||
npm install -g yarn@latest
|
||||
echo 5
|
||||
yarn >/dev/null
|
||||
export NODE_OPTIONS="--unhandled-rejections=strict"
|
||||
export JEST_OPTIONS="-i"
|
||||
export USE_OLD_PUPPETEER=1
|
||||
PROJECT_PATH=.. yarn build
|
||||
`, { assignFn: (x) => localPid = x, showOutputFn: () => remoteFailed });
|
||||
|
||||
if (!buildDone) {
|
||||
console.info('Local build finished, exit code:', localOutput.exitCode);
|
||||
if (localOutput.exitCode !== 0) {
|
||||
console.info(localOutput.text);
|
||||
localFailed = true;
|
||||
if (!remoteFailed) {
|
||||
return;
|
||||
} else {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
buildDone = true;
|
||||
await runLocalWithoutErrors(`
|
||||
rm -rf netlify/dist || true
|
||||
cp -r dist netlify
|
||||
mv netlify/dist/functions netlify/functions
|
||||
cp -r netlify/functions functions # Fix netlify bug
|
||||
`);
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.info('Ignore local build');
|
||||
}
|
||||
}
|
||||
const key = `
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
${(process.env.BUILDBOT_KEY || '').replace(/\s/g,'\n')}
|
||||
|
@ -166,7 +84,7 @@ ${(process.env.BUILDBOT_KEY || '').replace(/\s/g,'\n')}
|
|||
require('fs').writeFileSync('/tmp/buildbot', key);
|
||||
require('fs').chmodSync('/tmp/buildbot', 0o600);
|
||||
|
||||
const runRemote = async function(command, options) {
|
||||
const runRemote = async function(command, count = 3) {
|
||||
const bashCommand = `
|
||||
nocheck=" -o StrictHostKeyChecking=no "
|
||||
ssh -i /tmp/buildbot $nocheck ${remote} << 'EOSSH'
|
||||
|
@ -174,8 +92,14 @@ const runRemote = async function(command, options) {
|
|||
${command}
|
||||
EOSSH
|
||||
`
|
||||
return await runLocal(bashCommand, options);
|
||||
const result = await runLocal(bashCommand, true);
|
||||
if (result.exitCode === 255 && count > 0) {
|
||||
console.info(`Attempts to retry more: ${count}`);
|
||||
return await runRemote(command, count - 1);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const runRemoteWithoutErrors = async function(command) {
|
||||
const result = await runRemote(command);
|
||||
console.info(result.text.trim());
|
||||
|
@ -186,15 +110,8 @@ const runRemoteWithoutErrors = async function(command) {
|
|||
|
||||
const makeRemoteBuildWithCache = async function() {
|
||||
await runLocalWithoutErrors(`
|
||||
echo extracting
|
||||
mkdir tmpRemote
|
||||
cd tmpRemote
|
||||
rm -rf package || true
|
||||
npm pack interactive-landscape@${LANDSCAPEAPP}
|
||||
tar xzf interactive*.tgz
|
||||
cd ..
|
||||
mv tmpRemote/package packageRemote
|
||||
cp packageRemote/_yarn.lock packageRemote/yarn.lock
|
||||
rm -rf packageRemote || true
|
||||
git clone -b deploy --single-branch https://github.com/cncf/landscapeapp packageRemote
|
||||
`);
|
||||
|
||||
//how to get a hash based on our files
|
||||
|
@ -227,56 +144,6 @@ const makeRemoteBuildWithCache = async function() {
|
|||
const hash = getHash();
|
||||
const tmpHash = require('crypto').createHash('sha256').update(getTmpFile()).digest('hex');
|
||||
// lets guarantee npm install for this folder first
|
||||
{
|
||||
const buildCommand = [
|
||||
"(ls . ~/.nvm/nvm.sh || (curl -s -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash >/dev/null))",
|
||||
". ~/.nvm/nvm.sh",
|
||||
`nvm install ${nvmrc}`,
|
||||
`echo 0`,
|
||||
`nvm use ${nvmrc}`,
|
||||
`echo 1`,
|
||||
`npm install -g agentkeepalive --save`,
|
||||
`npm install -g npm --no-progress`,
|
||||
`npm install -g yarn@latest`,
|
||||
`cd /opt/repo/packageRemote`,
|
||||
`yarn >/dev/null`
|
||||
].join(' && ');
|
||||
const npmInstallCommand = `
|
||||
mkdir -p /root/builds/node_cache
|
||||
ls -l /root/builds/node_cache/${hash}/yarnLocal/unplugged 2>/dev/null || (
|
||||
mkdir -p /root/builds/node_cache/${tmpHash}/{yarnLocal,nvm,yarnGlobal}
|
||||
cp -r /root/builds/${folder}/packageRemote/.yarn/* /root/builds/node_cache/${tmpHash}/yarnLocal
|
||||
chmod -R 777 /root/builds/node_cache/${tmpHash}
|
||||
docker run --shm-size 1G --rm -t \
|
||||
-v /root/builds/node_cache/${tmpHash}/yarnLocal:/opt/repo/packageRemote/.yarn \
|
||||
-v /root/builds/node_cache/${tmpHash}/nvm:${dockerHome}/.nvm \
|
||||
-v /root/builds/node_cache/${tmpHash}/yarnGlobal:${dockerHome}/.yarn \
|
||||
-v /root/builds/${folder}:/opt/repo \
|
||||
${dockerImage} /bin/bash -lc "${buildCommand}"
|
||||
|
||||
ln -s /root/builds/node_cache/${tmpHash} /root/builds/node_cache/${hash} || (
|
||||
rm -rf /root/builds/node_cache/${tmpHash}
|
||||
)
|
||||
echo "packages for ${hash} had been installed"
|
||||
)
|
||||
chmod -R 777 /root/builds/node_cache/${hash}
|
||||
`;
|
||||
debug(npmInstallCommand);
|
||||
console.info(`Remote with cache: Installing npm packages if required`);
|
||||
const output = await runRemote(npmInstallCommand);
|
||||
console.info(`Remote with cache: Output from npm install: exit code: ${output.exitCode}`);
|
||||
if (output.exitCode !== 0) {
|
||||
console.info(output.text);
|
||||
throw new Error('Remote with cahce: npm install failed');
|
||||
}
|
||||
|
||||
const lines = output.text.split('\n');
|
||||
const index = lines.indexOf(lines.filter( (line) => line.match(/added \d+ packages in/))[0]);
|
||||
const filteredLines = lines.slice(index !== -1 ? index : 0).join('\n');
|
||||
console.info(filteredLines || 'Reusing an existing folder for node');
|
||||
|
||||
}
|
||||
|
||||
// do not pass REVIEW_ID because on failure we will run it locally and report
|
||||
// from there
|
||||
const vars = [
|
||||
|
@ -292,10 +159,16 @@ const makeRemoteBuildWithCache = async function() {
|
|||
const outputFolder = 'landscape' + getTmpFile();
|
||||
const buildCommand = [
|
||||
`cd /opt/repo/packageRemote`,
|
||||
"(ls . ~/.nvm/nvm.sh || (curl -s -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash >/dev/null))",
|
||||
`. ~/.nvm/nvm.sh`,
|
||||
`cat .nvmrc`,
|
||||
`nvm install ${nvmrc}`,
|
||||
`nvm use ${nvmrc}`,
|
||||
`npm install -g agentkeepalive --save`,
|
||||
`npm install -g npm@9 --no-progress`,
|
||||
`npm install -g yarn@latest`,
|
||||
`yarn`,
|
||||
`git config --global --add safe.directory /opt/repo`,
|
||||
`export NODE_OPTIONS="--unhandled-rejections=strict"`,
|
||||
`PROJECT_PATH=.. yarn run build`,
|
||||
`cp -r /opt/repo/dist /dist`
|
||||
|
@ -305,16 +178,12 @@ const makeRemoteBuildWithCache = async function() {
|
|||
mkdir -p /root/builds/${outputFolder}
|
||||
chmod -R 777 /root/builds/${outputFolder}
|
||||
chmod -R 777 /root/builds/${folder}
|
||||
chmod -R 777 /root/builds/node_cache/${hash}
|
||||
|
||||
docker run --shm-size 1G --rm -t \
|
||||
${vars.map( (v) => ` -e ${v}="${process.env[v]}" `).join(' ')} \
|
||||
-e NVM_NO_PROGRESS=1 \
|
||||
-e NETLIFY=1 \
|
||||
-e PARALLEL=TRUE \
|
||||
-v /root/builds/node_cache/${hash}/yarnLocal:/opt/repo/packageRemote/.yarn \
|
||||
-v /root/builds/node_cache/${hash}/nvm:${dockerHome}/.nvm \
|
||||
-v /root/builds/node_cache/${hash}/yarnGlobal:${dockerHome}/.yarn \
|
||||
-v /root/builds/${folder}:/opt/repo \
|
||||
-v /root/builds/${outputFolder}:/dist \
|
||||
${dockerImage} /bin/bash -lc "${buildCommand}"
|
||||
|
@ -346,31 +215,16 @@ const makeRemoteBuildWithCache = async function() {
|
|||
rsync -az --chmod=a+r -p -e "ssh -i /tmp/buildbot -o StrictHostKeyChecking=no " ${remote}:/root/builds/${outputFolder}/dist/* distRemote
|
||||
`
|
||||
));
|
||||
await runRemoteWithoutErrors(
|
||||
await runRemote(
|
||||
`
|
||||
rm -rf /root/builds/${folder}
|
||||
rm -rf /root/builds/${outputFolder}
|
||||
`
|
||||
)
|
||||
if (!buildDone) {
|
||||
buildDone = true;
|
||||
const newPids = await getPids();
|
||||
const pidsToKill = newPids.filter( (x) => !initialPids.includes(x));
|
||||
console.info(await runLocal(`kill -9 ${pidsToKill.join(' ')}`));
|
||||
|
||||
localPid.kill();
|
||||
|
||||
const pause = function(i) {
|
||||
return new Promise(function(resolve) {
|
||||
setTimeout(resolve, 5 * 1000);
|
||||
})
|
||||
};
|
||||
await pause(); // allow the previous process to be killed
|
||||
await runLocalWithoutErrors(`ps`);
|
||||
|
||||
console.info('Remote build done!');
|
||||
console.info(output.text);
|
||||
await runLocalWithoutErrors(`
|
||||
console.info('Remote build done!');
|
||||
console.info(output.text);
|
||||
await runLocalWithoutErrors(`
|
||||
rm -rf netlify/dist || true
|
||||
rm -rf dist || true
|
||||
mkdir -p netlify/dist
|
||||
|
@ -380,33 +234,24 @@ const makeRemoteBuildWithCache = async function() {
|
|||
mv netlify/dist/functions netlify/functions
|
||||
cp -r netlify/functions functions # Fix netlify bug
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const path = require('path');
|
||||
console.info('starting', process.cwd());
|
||||
process.chdir('..');
|
||||
await runLocal('rm package*.json');
|
||||
|
||||
initialPids = await getPids();
|
||||
|
||||
const cleanPromise = runRemoteWithoutErrors(`
|
||||
find builds/node_cache -maxdepth 1 -mtime +1 -exec rm -rf {} +;
|
||||
find builds/ -maxdepth 1 -not -path "builds/node_cache" -mtime +1 -exec rm -rf {} +;
|
||||
`).catch(function(ex) {
|
||||
`).catch(function() {
|
||||
console.info('Failed to clean up a builds folder');
|
||||
});
|
||||
|
||||
await Promise.all([makeRemoteBuildWithCache().catch(function(ex) {
|
||||
console.info('Remote build failed! Continuing with a local build', ex);
|
||||
remoteFailed = true;
|
||||
if (localFailed) {
|
||||
process.exit(1);
|
||||
}
|
||||
}), makeLocalBuild(), cleanPromise]);
|
||||
|
||||
console.info('build failed', ex);
|
||||
process.exit(1);
|
||||
}), cleanPromise]);
|
||||
}
|
||||
|
||||
main().catch(function(ex) {
|
||||
|
|
|
@ -1,9 +1,17 @@
|
|||
if (process.env.KEY3) {
|
||||
require('fs').mkdirSync(process.env.HOME + '/.ssh', { recursive: true});
|
||||
require('fs').writeFileSync(process.env.HOME + '/.ssh/bot3',
|
||||
"-----BEGIN RSA PRIVATE KEY-----\n" +
|
||||
process.env.KEY3.replace(/\s/g,"\n") +
|
||||
"\n-----END RSA PRIVATE KEY-----\n\n"
|
||||
);
|
||||
require('fs').chmodSync(process.env.HOME + '/.ssh/bot3', 0o600);
|
||||
console.info('Made a bot3 file');
|
||||
}
|
||||
|
||||
const path = require('path')
|
||||
const { readdirSync, writeFileSync } = require('fs')
|
||||
const generateIndex = require('./generateIndex')
|
||||
const run = function(x) {
|
||||
console.info(require('child_process').execSync(x).toString())
|
||||
}
|
||||
const debug = function() {
|
||||
if (process.env.DEBUG_BUILD) {
|
||||
console.info.apply(console, arguments);
|
||||
|
@ -16,18 +24,14 @@ const pause = function(i) {
|
|||
})
|
||||
};
|
||||
|
||||
console.info('starting', process.cwd());
|
||||
run('npm init -y');
|
||||
console.info('installing js-yaml', process.cwd());
|
||||
run('npm install js-yaml@4.0.0');
|
||||
const yaml = require('js-yaml');
|
||||
const yaml = require('./jsyaml');
|
||||
process.chdir('..');
|
||||
console.info('starting real script', process.cwd());
|
||||
const landscapesInfo = yaml.load(require('fs').readFileSync('landscapes.yml'));
|
||||
|
||||
const dockerImage = 'netlify/build:xenial';
|
||||
const dockerImage = 'netlify/build:focal';
|
||||
const dockerHome = '/opt/buildhome';
|
||||
|
||||
|
||||
async function main() {
|
||||
const nvmrc = require('fs').readFileSync('.nvmrc', 'utf-8').trim();
|
||||
const secrets = [
|
||||
|
@ -61,7 +65,7 @@ ${process.env.BUILDBOT_KEY.replace(/\s/g,'\n')}
|
|||
|
||||
// now our goal is to run this on a remote server. Step 1 - xcopy the repo
|
||||
const folder = new Date().getTime();
|
||||
const remote = 'root@147.75.76.177';
|
||||
const remote = 'root@147.75.199.15';
|
||||
|
||||
const runRemote = async function(command) {
|
||||
const bashCommand = `
|
||||
|
@ -74,6 +78,24 @@ EOSSH
|
|||
const result = await runLocal(bashCommand);
|
||||
let newOutput = [];
|
||||
for (var l of result.text.split('\n')) {
|
||||
if (l.match(/Counting objects: /)) {
|
||||
continue;
|
||||
}
|
||||
if (l.match(/ExperimentalWarning: Custom ESM Loaders is an experimental feature./)) {
|
||||
continue
|
||||
}
|
||||
if (l.match(/Compressing objects: /)) {
|
||||
continue;
|
||||
}
|
||||
if (l.match(/Receiving objects: /)) {
|
||||
continue;
|
||||
}
|
||||
if (l.match(/Resolving deltas: /)) {
|
||||
continue;
|
||||
}
|
||||
if (l.match(/Could not resolve ".*?" in file/)) {
|
||||
continue;
|
||||
}
|
||||
newOutput.push(l);
|
||||
if (l.includes('mesg: ttyname failed: Inappropriate ioctl for device')) {
|
||||
newOutput = [];
|
||||
|
@ -154,7 +176,6 @@ EOSSH
|
|||
await runRemoteWithoutErrors(`chmod -R 777 /root/builds/${folder}`);
|
||||
|
||||
// lets guarantee npm install for this folder first
|
||||
const branch = process.env.BRANCH;
|
||||
{
|
||||
const buildCommand = [
|
||||
"(ls . ~/.nvm/nvm.sh || (curl -s -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash >/dev/null))",
|
||||
|
@ -163,7 +184,8 @@ EOSSH
|
|||
`nvm use ${nvmrc}`,
|
||||
`npm install -g yarn --no-progress --silent`,
|
||||
`cd /opt/repo`,
|
||||
`yarn >/dev/null`
|
||||
`yarn >/dev/null`,
|
||||
`yarn eslint`
|
||||
].join(' && ');
|
||||
const npmInstallCommand = `
|
||||
mkdir -p /root/builds/${folder}_node
|
||||
|
@ -194,6 +216,7 @@ EOSSH
|
|||
const outputFolder = landscape.name + new Date().getTime();
|
||||
const buildCommand = [
|
||||
`cd /opt/repo`,
|
||||
`git config --global --add safe.directory /opt/repo`,
|
||||
`. ~/.nvm/nvm.sh`,
|
||||
`nvm use`,
|
||||
`export NODE_OPTIONS="--unhandled-rejections=strict"`,
|
||||
|
@ -234,8 +257,12 @@ EOSSH
|
|||
|
||||
output = await runRemote(dockerCommand);
|
||||
output.landscape = landscape;
|
||||
console.info(`Output from: ${output.landscape.name}, exit code: ${output.exitCode}`);
|
||||
console.info(output.text);
|
||||
if (output.exitCode) {
|
||||
console.info(`Output from: ${output.landscape.name}, exit code: ${output.exitCode}`);
|
||||
console.info(output.text);
|
||||
} else {
|
||||
console.info(`Done: ${output.landscape.name}`);
|
||||
}
|
||||
if (output.exitCode === 255) { // a single ssh failure
|
||||
output = await runRemote(dockerCommand);
|
||||
output.landscape = landscape;
|
||||
|
@ -297,24 +324,18 @@ EOSSH
|
|||
await runLocalWithoutErrors('cp -r dist netlify');
|
||||
|
||||
if (process.env.BRANCH === 'master') {
|
||||
console.info(await runLocal('git remote -v'));
|
||||
await runLocalWithoutErrors(`
|
||||
git config --global user.email "info@cncf.io"
|
||||
git config --global user.name "CNCF-bot"
|
||||
git remote rm github 2>/dev/null || true
|
||||
git remote add github "https://$GITHUB_USER:$GITHUB_TOKEN@github.com/cncf/landscapeapp"
|
||||
git fetch github
|
||||
# git diff # Need to comment this when a diff is too large
|
||||
git checkout -- .
|
||||
npm version patch || npm version patch || npm version patch
|
||||
git commit -m 'Update to a new version [skip ci]' --allow-empty --amend
|
||||
git branch -D tmp || true
|
||||
git checkout -b tmp
|
||||
git push github HEAD:master || true
|
||||
git push github HEAD:master --tags --force
|
||||
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/.npmrc
|
||||
git diff
|
||||
npm -q publish || (sleep 5 && npm -q publish) || (sleep 30 && npm -q publish)
|
||||
echo 'Npm package published'
|
||||
git remote add github "git@github.com:cncf/landscapeapp.git"
|
||||
echo 1
|
||||
GIT_SSH_COMMAND='ssh -i ~/.ssh/bot3 -o IdentitiesOnly=yes' git fetch github
|
||||
echo 2
|
||||
git --no-pager show HEAD
|
||||
echo 3
|
||||
GIT_SSH_COMMAND='ssh -i ~/.ssh/bot3 -o IdentitiesOnly=yes' git push github github/master:deploy
|
||||
`);
|
||||
// just for debug purpose
|
||||
//now we have a different hash, because we updated a version, but for build purposes we have exactly same npm modules
|
||||
|
|
|
@ -0,0 +1,176 @@
|
|||
// this "server.js" script should be able to run everything itself, without
|
||||
// having to bother with any packages or similar problems.
|
||||
const fs = require('fs/promises');
|
||||
const path = require('path');
|
||||
|
||||
const oldCreateConnection = require('https').globalAgent.createConnection;
|
||||
require('https').globalAgent.createConnection = function(options, cb) {
|
||||
options.highWaterMark = 1024 * 1024;
|
||||
options.readableHighWaterMark = 1024 * 1024;
|
||||
return oldCreateConnection.apply(this, [options, cb]);
|
||||
}
|
||||
|
||||
|
||||
// we will get a content of all files, in a form of entries
|
||||
// "file", "content", "md5"
|
||||
async function getContent() {
|
||||
const dirs = ["images", "hosted_logos", "cached_logos"];
|
||||
const files = ["landscape.yml", "settings.yml", "processed_landscape.yml", "guide.md"];
|
||||
const all = [];
|
||||
for (let dir of dirs) {
|
||||
const filesInDir = await fs.readdir(dir);
|
||||
for (let file of filesInDir) {
|
||||
if (file !== '.' && file !== '..') {
|
||||
const content = await fs.readFile(`${dir}/${file}`, { encoding: 'base64'});
|
||||
const md5 = require('crypto').createHash('md5').update(content).digest("hex");
|
||||
all.push({
|
||||
file: `${dir}/${file}`,
|
||||
content: content,
|
||||
md5: md5
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let file of files) {
|
||||
let content;
|
||||
try {
|
||||
content = await fs.readFile(file, { encoding: 'base64'});
|
||||
} catch(ex) {
|
||||
|
||||
}
|
||||
if (content) {
|
||||
const md5 = require('crypto').createHash('md5').update(content).digest("hex");
|
||||
all.push({
|
||||
file: file,
|
||||
content: content,
|
||||
md5: md5
|
||||
});
|
||||
}
|
||||
}
|
||||
return all;
|
||||
}
|
||||
|
||||
function get(path) {
|
||||
return new Promise(function(resolve) {
|
||||
const base = process.env.DEBUG_SERVER ? 'http://localhost:3000' : 'https://weblandscapes.ddns.net';
|
||||
const http = require(base.indexOf('http://') === 0 ? 'http' : 'https');
|
||||
const originalPath = path;
|
||||
path = `${base}/api/console/download/${path}`;
|
||||
const req = http.request(path, function(res) {
|
||||
const path1 = originalPath.replace('?', '.html?');
|
||||
if (res.statusCode === 404 && path.indexOf('.html') === -1) {
|
||||
get(path1).then( (x) => resolve(x));
|
||||
} else {
|
||||
resolve({
|
||||
res: res,
|
||||
headers: res.headers,
|
||||
statusCode: res.statusCode
|
||||
});
|
||||
}
|
||||
});
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
function post({path, request}) {
|
||||
return new Promise(function(resolve) {
|
||||
const base = process.env.DEBUG_SERVER ? 'http://localhost:3000' : 'https://weblandscapes.ddns.net';
|
||||
const http = require(base.indexOf('http://') === 0 ? 'http' : 'https');
|
||||
|
||||
let data = '';
|
||||
const req = http.request({
|
||||
hostname: base.split('://')[1].replace(':3000', ''),
|
||||
port: base.indexOf('3000') !== -1 ? '3000' : 443,
|
||||
path: path,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json; charset=UTF-8'
|
||||
}
|
||||
}, function(res) {
|
||||
res.on('data', function(chunk) {
|
||||
data += chunk;
|
||||
});
|
||||
res.on('close', function() {
|
||||
resolve(JSON.parse(data));
|
||||
});
|
||||
});
|
||||
req.write(JSON.stringify(request));
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// a build is done on a server side, instead of a client side, this
|
||||
// is a key moment
|
||||
// 1. get content (that is fast)
|
||||
// 2. send a list of hashes
|
||||
// 3. get back a list of existing hashes
|
||||
// 4. send a list of (file, content, hash) , but skip the content when a hash is on the server
|
||||
// 5. get a build result on a server
|
||||
let previousGlobalHash;
|
||||
let lastOutput;
|
||||
let currentPath;
|
||||
async function build() {
|
||||
const files = await getContent();
|
||||
if (!previousGlobalHash) {
|
||||
console.info(`Starting a new build...`);
|
||||
}
|
||||
if (JSON.stringify(files.map( (x) => x.md5)) === previousGlobalHash) {
|
||||
return;
|
||||
}
|
||||
if (previousGlobalHash) {
|
||||
console.info(`Changes detected, starting a new build`);
|
||||
}
|
||||
previousGlobalHash = JSON.stringify(files.map( (x) => x.md5));
|
||||
const availableIds = await post({path: '/api/console/ids', request: { ids: files.map( (x) => x.md5 ) }});
|
||||
const availableSet = new Set(availableIds.existingIds);
|
||||
const filesWithoutExtraContent = files.map( (file) => ({
|
||||
file: file.file,
|
||||
md5: file.md5,
|
||||
content: availableSet.has(file.md5) ? '' : file.content
|
||||
}));
|
||||
const result = await post({path: '/api/console/preview', request: { files: filesWithoutExtraContent }});
|
||||
if (result.success) {
|
||||
currentPath = result.path;
|
||||
console.info(`${new Date().toISOString()} build result: ${result.success ? 'success' : 'failure'} `);
|
||||
} else {
|
||||
lastOutput = result.output;
|
||||
console.info(`${new Date().toISOString()} build result: ${result.success ? 'success' : 'failure'} `);
|
||||
console.info(result.output);
|
||||
}
|
||||
}
|
||||
|
||||
function server() {
|
||||
const http = require('http');
|
||||
http.createServer(async function (request, response) {
|
||||
if (!currentPath) {
|
||||
response.writeHead(404);
|
||||
if (lastOutput) {
|
||||
response.end(lastOutput);
|
||||
} else {
|
||||
response.end('Site is not ready');
|
||||
}
|
||||
} else {
|
||||
let filePath = request.url.split('?')[0];
|
||||
const url = path.join(currentPath, 'dist', filePath + '?' + request.url.split('?')[1]);
|
||||
console.info(`Fetching ${url}`);
|
||||
const output = await get(url);
|
||||
response.writeHead(output.statusCode, output.headers);
|
||||
output.res.pipe(response);
|
||||
}
|
||||
|
||||
}).listen(process.env.PORT || 8001);
|
||||
console.log(`Development server running at http://127.0.0.1:${process.env.PORT || 8001}/`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
server();
|
||||
//eslint-disable-next-line no-constant-condition
|
||||
while(true) {
|
||||
await build();
|
||||
}
|
||||
}
|
||||
main().catch(function(ex) {
|
||||
console.info(ex);
|
||||
});
|
||||
// how will a server work?
|
|
@ -1,37 +0,0 @@
|
|||
const path = require('path')
|
||||
const { readFileSync } = require('fs')
|
||||
const { load } = require('js-yaml')
|
||||
const bundleAnalyzerPlugin = require('@next/bundle-analyzer')
|
||||
const getBasePath = require('./tools/getBasePath')
|
||||
|
||||
const withBundleAnalyzer = bundleAnalyzerPlugin({ enabled: !!process.env.ANALYZE })
|
||||
|
||||
const projectPath = process.env.PROJECT_PATH
|
||||
|
||||
const lastUpdated = new Date().toISOString().substring(0, 19).replace('T', ' ') + 'Z'
|
||||
|
||||
const processedLandscape = load(readFileSync(path.resolve(projectPath, 'processed_landscape.yml')));
|
||||
const tweets = (processedLandscape.twitter_options || {}).count || 0
|
||||
|
||||
const GA = process.env.GA
|
||||
|
||||
const basePath = getBasePath()
|
||||
|
||||
module.exports = withBundleAnalyzer({
|
||||
env: { lastUpdated, tweets, GA, PROJECT_NAME: process.env.PROJECT_NAME },
|
||||
basePath,
|
||||
webpack: (config, options) => {
|
||||
// CAREFUL before adding more presets, next/babel already includes some
|
||||
// see https://nextjs.org/docs/advanced-features/customizing-babel-config
|
||||
|
||||
config.externals = [
|
||||
...config.externals,
|
||||
{ moment: 'moment' }
|
||||
]
|
||||
if (process.env.PREVIEW) {
|
||||
config.optimization.minimize = false;
|
||||
}
|
||||
|
||||
return config
|
||||
},
|
||||
})
|
170
package.json
170
package.json
|
@ -1,142 +1,82 @@
|
|||
{
|
||||
"name": "interactive-landscape",
|
||||
"version": "1.0.619",
|
||||
"version": "1.0.657",
|
||||
"description": "Visualization tool for building interactive landscapes",
|
||||
"engines": {
|
||||
"npm": ">=3",
|
||||
"node": ">= 10.5"
|
||||
},
|
||||
"scripts": {
|
||||
"autocrop-images": "babel-node tools/autocropImages",
|
||||
"dev": "yarn yaml2json && yarn watch-landscape",
|
||||
"open:src": "yarn dev",
|
||||
"open:dist": "yarn stop-old-ci && babel-node tools/distServer.js",
|
||||
"get-iframe-resizer-path": "node -e \"console.info(require.resolve('iframe-resizer/js/iframeResizer.min.js'))\" ",
|
||||
"landscapes": "babel-node tools/landscapes.js",
|
||||
"lint": "esw webpack.config.* src tools --color",
|
||||
"lint:watch": "yarn lint -- --watch",
|
||||
"eslint": "eslint src tools specs netlify",
|
||||
"autocrop-images": "node tools/autocropImages",
|
||||
"dev": "yarn server",
|
||||
"open:src": "yarn server",
|
||||
"server": "node server.js",
|
||||
"landscapes": "node tools/landscapes.js",
|
||||
"update-github-colors": "curl https://raw.githubusercontent.com/Diastro/github-colors/master/github-colors.json > tools/githubColors.json",
|
||||
"fetch": "babel-node tools/migrate.js && babel-node tools/validateLandscape && babel-node tools/checkWrongCharactersInFilenames && babel-node tools/addExternalInfo.js && yarn yaml2json",
|
||||
"fetch": "node tools/validateLandscape && node tools/checkWrongCharactersInFilenames && node tools/addExternalInfo.js && yarn yaml2json",
|
||||
"fetchAll": "LEVEL=complete yarn fetch",
|
||||
"update": "(rm /tmp/landscape.json || true) && babel-node tools/migrate.js && babel-node tools/validateLandscape && yarn remove-quotes && LEVEL=medium babel-node tools/addExternalInfo.js && yarn prune && yarn check-links && yarn yaml2json && babel-node tools/calculateNumberOfTweets && babel-node tools/updateTimestamps",
|
||||
"yaml2json": "babel-node tools/generateJson.js",
|
||||
"remove-quotes": "babel-node tools/removeQuotes",
|
||||
"prune": "babel-node tools/pruneExtraEntries",
|
||||
"check-links": "babel-node tools/checkLinks",
|
||||
"remove-dist": "rimraf \"$PROJECT_PATH\"/dist",
|
||||
"light-update": "(rm /tmp/landscape.json || true) && node tools/validateLandscape && yarn remove-quotes && LEVEL=crunchbase node tools/addExternalInfo.js && yarn prune && yarn yaml2json && node tools/updateTimestamps",
|
||||
"update": "(rm /tmp/landscape.json || true) && node tools/validateLandscape && yarn remove-quotes && LEVEL=medium node tools/addExternalInfo.js && yarn prune && yarn check-links && yarn yaml2json && node tools/updateTimestamps",
|
||||
"yaml2json": "node tools/generateJson.js",
|
||||
"remove-quotes": "node tools/removeQuotes",
|
||||
"prune": "node tools/pruneExtraEntries",
|
||||
"check-links": "node tools/checkLinks",
|
||||
"precommit": "yarn fetch",
|
||||
"host-images": "babel-node tools/hostImages.js && yarn fetch",
|
||||
"start-ci": "yarn exec bash -c \"(yarn run babel-node tools/distServer.js &) && sleep 10\"",
|
||||
"stop-old-ci": "yarn run babel-node tools/stopOldDistServer.js",
|
||||
"start-ci": "yarn exec bash -c \"(node tools/distServer.js &) && sleep 10\"",
|
||||
"stop-old-ci": "node tools/stopOldDistServer.js",
|
||||
"stop-ci": "yarn exec bash -c \"kill -9 `cat /tmp/ci.pid` >/dev/null 2>/dev/null && rm /tmp/ci.pid \"",
|
||||
"integration-test": "jest ${JEST_OPTIONS:-} --reporters=./tools/jestReporter.js --reporters=jest-standard-reporter",
|
||||
"check-landscape": "babel-node tools/checkLandscape",
|
||||
"render-landscape": "babel-node tools/renderLandscape",
|
||||
"funding": "babel-node tools/fundingForMasterBranch",
|
||||
"copy-dist": "cp -r _headers out && ([ -z \"${PROJECT_NAME:-}\" ] || mkdir \"$PROJECT_PATH\"/dist ) && cp -r out/ \"$PROJECT_PATH\"/dist/${PROJECT_NAME:-}",
|
||||
"prepare-landscape": "babel-node tools/prepareLandscape.js",
|
||||
"watch-landscape": "babel-node tools/watchLandscape.js",
|
||||
"setup-robots": "babel-node tools/sitemap && babel-node tools/addRobots",
|
||||
"quick-build": "yarn build-next && yarn setup-robots && yarn copy-dist ",
|
||||
"build": "yarn fetch && yarn prepare-landscape && yarn build-next && yarn setup-robots && yarn remove-dist && yarn copy-dist && yarn export-functions && yarn stop-old-ci && yarn start-ci && babel-node tools/parallelWithRetry integration-test check-landscape render-landscape funding && yarn stop-ci",
|
||||
"build-next": "next build && next export",
|
||||
"export-functions": "babel-node ./tools/exportFunctions",
|
||||
"show-report": "open dist/report.html",
|
||||
"analyze-bundle": "babel-node ./tools/analyzeBundle.js",
|
||||
"integration-test": "jest --runInBand --reporters=jest-standard-reporter",
|
||||
"test": "jest",
|
||||
"check-landscape": "node tools/checkLandscape",
|
||||
"render-landscape": "node tools/renderLandscape",
|
||||
"funding": "node tools/fundingForMasterBranch",
|
||||
"prepare-landscape": "node tools/prepareLandscape.js && node tools/renderItems.js",
|
||||
"setup-robots": "node tools/sitemap && node tools/addRobots",
|
||||
"build": "yarn fetch && yarn prepare-landscape && node tools/renderAcquisitions && yarn setup-robots && yarn export-functions && yarn stop-old-ci && yarn start-ci && node tools/parallelWithRetry integration-test check-landscape render-landscape funding && yarn stop-ci",
|
||||
"export-functions": "node ./tools/exportFunctions",
|
||||
"latest": "yarn",
|
||||
"reset-tweet-count": "babel-node tools/resetTweetCount.js",
|
||||
"reset-tweet-count": "node tools/resetTweetCount.js",
|
||||
"prepublish": "cp yarn.lock _yarn.lock",
|
||||
"postpublish": "rm _yarn.lock || true",
|
||||
"preview": "yarn fetch && yarn prepare-landscape && yarn build-next"
|
||||
"preview": "yarn fetch && yarn prepare-landscape && yarn export-functions"
|
||||
},
|
||||
"author": "CNCF",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.16.0",
|
||||
"@babel/node": "^7.16.0",
|
||||
"@babel/register": "^7.16.0",
|
||||
"@babel/runtime": "^7.16.3",
|
||||
"@date-io/date-fns": "^2.11.0",
|
||||
"@material-ui/core": "^4.12.3",
|
||||
"@material-ui/icons": "^4.11.2",
|
||||
"@material-ui/lab": "^4.0.0-alpha.60",
|
||||
"@material-ui/pickers": "^3.3.10",
|
||||
"@next/bundle-analyzer": "^11.1.0",
|
||||
"@vercel/ncc": "^0.33.0",
|
||||
"@vercel/ncc": "^0.34.0",
|
||||
"anchorme": "^2.1.2",
|
||||
"axe-puppeteer": "^1.1.1",
|
||||
"axios": "^0.24.0",
|
||||
"babel-plugin-module-resolver": "^4.1.0",
|
||||
"axios": "^0.27.2",
|
||||
"bluebird": "3.7.2",
|
||||
"change-case": "^4.1.2",
|
||||
"chart.js": "^3.6.2",
|
||||
"chartjs-adapter-date-fns": "^2.0.0",
|
||||
"cheerio": "^1.0.0-rc.10",
|
||||
"chokidar": "^3.5.2",
|
||||
"classnames": "^2.3.1",
|
||||
"cheerio": "^1.0.0-rc.11",
|
||||
"colors": "1.4.0",
|
||||
"compression": "^1.7.4",
|
||||
"current-device": "^0.10.2",
|
||||
"date-fns": "^2.27.0",
|
||||
"debug": "^4.3.3",
|
||||
"ejs": "^3.1.6",
|
||||
"eslint": "^8.4.0",
|
||||
"eslint-plugin-import": "^2.25.3",
|
||||
"eslint-plugin-react": "^7.27.1",
|
||||
"eslint-watch": "^8.0.0",
|
||||
"debug": "^4.3.4",
|
||||
"eslint": "latest",
|
||||
"event-emitter": "0.3.5",
|
||||
"expect-puppeteer": "^6.0.2",
|
||||
"express": "^4.17.1",
|
||||
"extract-zip": "2.0.1",
|
||||
"favicons-webpack-plugin": "^5.0.2",
|
||||
"expect-puppeteer": "^6.1.0",
|
||||
"feed": "^4.2.2",
|
||||
"fontsource-roboto": "^4.0.0",
|
||||
"format-number": "3.0.0",
|
||||
"get-contrast-ratio": "^0.2.1",
|
||||
"git-branch": "2.0.1",
|
||||
"iframe-resizer": "^4.3.2",
|
||||
"jest": "^27.4.3",
|
||||
"jest-cli": "^27.4.3",
|
||||
"jest-puppeteer": "^6.0.2",
|
||||
"jest": "^28.1.0",
|
||||
"jest-cli": "^28.1.0",
|
||||
"jest-standard-reporter": "^2.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsdom": "^19.0.0",
|
||||
"json2csv": "^5.0.6",
|
||||
"json2csv": "^5.0.7",
|
||||
"lodash": "^4.17.21",
|
||||
"measure-text-width": "0.0.4",
|
||||
"millify": "^4.0.0",
|
||||
"next": "^12.0.7",
|
||||
"node-emoji": "^1.11.0",
|
||||
"oauth-1.0a": "^2.2.6",
|
||||
"open": "^8.4.0",
|
||||
"postcss": "^8.4.4",
|
||||
"postcss-import": "^14.0.2",
|
||||
"postcss-loader": "^6.2.1",
|
||||
"postcss-nested": "4",
|
||||
"postcss-simple-vars": "^6.0.3",
|
||||
"prop-types": "15.7.2",
|
||||
"puppeteer": "^12.0.1",
|
||||
"query-string": "^7.0.1",
|
||||
"raf": "3.4.1",
|
||||
"react": "^17.0.2",
|
||||
"react-chartjs-2": "^3.0.4",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-ga": "^3.3.0",
|
||||
"react-hot-loader": "^4.13.0",
|
||||
"react-key-handler": "^1.2.0-beta.3",
|
||||
"react-test-renderer": "^17.0.2",
|
||||
"react-twitter-widgets": "^1.10.0",
|
||||
"recompose": "0.30.0",
|
||||
"puppeteer": "^14.2.1",
|
||||
"query-string": "^7.1.1",
|
||||
"relative-date": "1.1.3",
|
||||
"reselect": "^4.1.5",
|
||||
"rimraf": "3.0.2",
|
||||
"sanitize-html": "^2.6.0",
|
||||
"serve-static": "1.14.1",
|
||||
"showdown": "^1.9.1",
|
||||
"sitemap": "^7.0.0",
|
||||
"svg-autocrop": "^2.0.40",
|
||||
"swr": "^0.5.6",
|
||||
"sanitize-html": "^2.7.0",
|
||||
"showdown": "^2.1.0",
|
||||
"sitemap": "^7.1.1",
|
||||
"svg-autocrop": "^2.0.41",
|
||||
"traverse": "0.6.6",
|
||||
"yarn": "^1.22.17"
|
||||
"yarn": "^1.22.18"
|
||||
},
|
||||
"keywords": [
|
||||
"landscape",
|
||||
|
@ -153,25 +93,5 @@
|
|||
"type": "git",
|
||||
"url": "https://github.com/cncf/landscapeapp"
|
||||
},
|
||||
"jest": {
|
||||
"haste": {},
|
||||
"testPathIgnorePatterns": [
|
||||
"<rootDir>/node_modules"
|
||||
],
|
||||
"transformIgnorePatterns": [
|
||||
"<rootDir>/node_modules"
|
||||
],
|
||||
"moduleNameMapper": {
|
||||
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/tools/assetsTransformer.js",
|
||||
"\\.(css)$": "<rootDir>/tools/assetsTransformer.js"
|
||||
},
|
||||
"setupFiles": [
|
||||
"raf/polyfill"
|
||||
]
|
||||
},
|
||||
"dependenciesMeta": {
|
||||
"open": {
|
||||
"unplugged": true
|
||||
}
|
||||
}
|
||||
"packageManager": "yarn@3.2.1"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,119 @@
|
|||
// acts as a dev server and a dist server
|
||||
// netlify does not use this
|
||||
|
||||
const projectPath = process.env.PROJECT_PATH;
|
||||
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const fnFile = (file) => {
|
||||
const functionsPath = path.join(projectPath, 'dist', process.env.PROJECT_NAME || '', 'functions' );
|
||||
const destFile = [process.env.PROJECT_NAME, file].filter(_ => _).join('--');
|
||||
return path.join(functionsPath, destFile);
|
||||
}
|
||||
|
||||
http.createServer(function (request, response) {
|
||||
|
||||
if (request.url.indexOf('/api/ids') !== -1) {
|
||||
console.log('api request starting...', request.url);
|
||||
const query = request.url.split('?')[1] || '';
|
||||
|
||||
if (!process.env.INLINE_API) {
|
||||
require('child_process').exec(`node ${fnFile("ids.js")} '${query}'`, {}, function(e, output, err) {
|
||||
console.info(err);
|
||||
response.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
response.end(output);
|
||||
});
|
||||
} else {
|
||||
const output = require('./src/api/ids.js').processRequest(query);
|
||||
response.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
response.end(JSON.stringify(output));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (request.url.indexOf('/api/export') !== -1) {
|
||||
console.log('api request starting...', request.url);
|
||||
const query = request.url.split('?')[1] || '';
|
||||
|
||||
if (!process.env.INLINE_API) {
|
||||
require('child_process').exec(`node ${fnFile("export.js")} '${query}'`, {}, function(e, output, err) {
|
||||
response.writeHead(200, {
|
||||
'Content-Type': 'text/css',
|
||||
'Content-Disposition': 'attachment; filename=interactive-landscape.csv'
|
||||
});
|
||||
response.end(output);
|
||||
});
|
||||
} else {
|
||||
const output = require('./src/api/export.js').processRequest(query);
|
||||
response.writeHead(200, {
|
||||
'Content-Type': 'text/css',
|
||||
'Content-Disposition': 'attachment; filename=interactive-landscape.csv'
|
||||
});
|
||||
response.end(output);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let filePath = path.join(process.env.PROJECT_PATH, 'dist', request.url.split('?')[0]);
|
||||
if (fs.existsSync(path.resolve(filePath, 'index.html'))) {
|
||||
filePath = path.resolve(filePath, 'index.html');
|
||||
} else if (fs.existsSync(filePath + '.html')) {
|
||||
filePath = filePath + '.html'
|
||||
}
|
||||
|
||||
const extname = path.extname(filePath);
|
||||
let encoding = 'utf-8';
|
||||
var contentType = 'text/html; charset=utf-8';
|
||||
switch (extname) {
|
||||
case '.js':
|
||||
contentType = 'text/javascript; charset=utf-8';
|
||||
break;
|
||||
case '.css':
|
||||
contentType = 'text/css; charset=utf-8';
|
||||
break;
|
||||
case '.json':
|
||||
contentType = 'application/json; charset=utf-8';
|
||||
break;
|
||||
case '.svg':
|
||||
contentType = 'image/svg+xml; charset=utf-8';
|
||||
break;
|
||||
case '.jpg':
|
||||
contentType = 'image/jpg';
|
||||
encoding = undefined;
|
||||
break;
|
||||
case '.png':
|
||||
contentType = 'image/png';
|
||||
encoding = undefined;
|
||||
break;
|
||||
case '.pdf':
|
||||
contentType = 'application/pdf';
|
||||
encoding = undefined;
|
||||
break;
|
||||
}
|
||||
|
||||
fs.readFile(filePath, encoding, function(error, content) {
|
||||
if (error) {
|
||||
const extraPath = filePath + '.html';
|
||||
fs.readFile(extraPath, encoding, function(error, content) {
|
||||
if (error) {
|
||||
if(error.code == 'ENOENT') {
|
||||
response.writeHead(200, { 'Content-Type': contentType });
|
||||
response.end('404. Not found. ', 'utf-8');
|
||||
} else {
|
||||
response.writeHead(500);
|
||||
response.end('Sorry, check with the site admin for error: '+error.code+' ..\n');
|
||||
response.end();
|
||||
}
|
||||
} else {
|
||||
response.writeHead(200, { 'Content-Type': contentType });
|
||||
response.end(content, 'utf-8');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
response.writeHead(200, { 'Content-Type': contentType });
|
||||
response.end(content, 'utf-8');
|
||||
}
|
||||
});
|
||||
}).listen(process.env.PORT || 8001);
|
||||
console.log(`Development server running at http://127.0.0.1:${process.env.PORT || 8001}/`);
|
|
@ -0,0 +1,9 @@
|
|||
module.exports = {
|
||||
globals: {
|
||||
jest: true,
|
||||
test: true,
|
||||
it: true,
|
||||
describe: true,
|
||||
expect: true
|
||||
}
|
||||
};
|
|
@ -1,47 +0,0 @@
|
|||
import { existsSync } from 'fs'
|
||||
import puppeteer from "puppeteer";
|
||||
const { AxePuppeteer } = require('axe-puppeteer');
|
||||
import { appUrl } from '../tools/distSettings'
|
||||
import { projectPath } from '../tools/settings'
|
||||
|
||||
const analyzePage = async url => {
|
||||
const browser = await puppeteer.launch({args: ['--no-sandbox', '--disable-setuid-sandbox'], defaultViewport: { width: 1600, height: 1200 }});
|
||||
const page = await browser.newPage();
|
||||
await page.goto(url);
|
||||
await page.waitForSelector('.app')
|
||||
const results = await new AxePuppeteer(page).withTags('wcag2a').analyze()
|
||||
await browser.close()
|
||||
|
||||
if (results.violations.length > 0) {
|
||||
const output = [
|
||||
'Encountered the following accessibility issues:',
|
||||
...results.violations.flatMap(violation => {
|
||||
return [
|
||||
'',
|
||||
`[${violation.impact}] ${violation.help}:`,
|
||||
` DESCRIPTION: ${violation.helpUrl}`,
|
||||
' ELEMENTS:',
|
||||
...violation.nodes.flatMap(node => ` * ${node.html}`),
|
||||
]
|
||||
})
|
||||
].join('\n')
|
||||
|
||||
throw output
|
||||
}
|
||||
}
|
||||
|
||||
describe("Accessibility", () => {
|
||||
test("Main Landscape", async () => {
|
||||
await analyzePage(appUrl)
|
||||
}, 60 * 1000);
|
||||
|
||||
test("Card Mode", async () => {
|
||||
await analyzePage(`${appUrl}/card-mode`)
|
||||
}, 60 * 1000);
|
||||
|
||||
if (existsSync(`${projectPath}/guide`)) {
|
||||
test("Guide", async () => {
|
||||
await analyzePage(`${appUrl}/guide`)
|
||||
}, 60 * 1000);
|
||||
}
|
||||
});
|
|
@ -1,16 +1,14 @@
|
|||
import puppeteer from "puppeteer";
|
||||
const puppeteer = require("puppeteer");
|
||||
require('expect-puppeteer');
|
||||
import { paramCase } from 'change-case';
|
||||
import { settings } from '../tools/settings';
|
||||
import { projects } from '../tools/loadData';
|
||||
import { landscapeSettingsList } from "../src/utils/landscapeSettings";
|
||||
import { appUrl, pathPrefix } from '../tools/distSettings'
|
||||
const { paramCase } = require('change-case');
|
||||
const { settings } = require('../tools/settings');
|
||||
const { projects } = require('../tools/loadData');
|
||||
const { landscapeSettingsList } = require("../src/utils/landscapeSettings");
|
||||
const { appUrl, pathPrefix } = require('../tools/distSettings');
|
||||
|
||||
const devicesMap = puppeteer.devices;
|
||||
const width = 1920;
|
||||
const height = 1080;
|
||||
|
||||
let setup;
|
||||
let browser;
|
||||
let page;
|
||||
let close = () => test('Closing a browser', async () => await browser.close());
|
||||
|
@ -26,148 +24,142 @@ expect.extend({
|
|||
return { pass, message };
|
||||
},
|
||||
})
|
||||
|
||||
jest.setTimeout(process.env.SHOW_BROWSER ? 40000 : 30000);
|
||||
jest.setTimeout(process.env.SHOW_BROWSER ? 30000 : 30000);
|
||||
|
||||
async function makePage(initialUrl) {
|
||||
try {
|
||||
browser = await puppeteer.launch({args: ['--no-sandbox', '--disable-setuid-sandbox'], headless: !process.env.SHOW_BROWSER});
|
||||
const page = await browser.newPage();
|
||||
await setup(page);
|
||||
await page.goto(initialUrl);
|
||||
await page.setViewport({ width, height });
|
||||
return page;
|
||||
} catch(ex) {
|
||||
try {
|
||||
console.info('retrying...', ex);
|
||||
browser.close();
|
||||
} catch(ex2) {
|
||||
|
||||
console.info('failed to close browser', ex2);
|
||||
}
|
||||
return await makePage(initialUrl);
|
||||
}
|
||||
}
|
||||
|
||||
function embedTest() {
|
||||
describe("Embed test", () => {
|
||||
describe("I visit an example embed page", () => {
|
||||
let frame;
|
||||
test('page is open and has a frame', async function(){
|
||||
page = await makePage(appUrl + '/embed');
|
||||
frame = await page.frames()[1];
|
||||
await frame.waitForXPath(`//h1[contains(text(), 'full interactive landscape')]`);
|
||||
});
|
||||
|
||||
test('Do not see a content from a main mode', async function() {
|
||||
const title = await frame.$('h1', { text: settings.test.header })
|
||||
expect(await title.boundingBox()).toBe(null)
|
||||
});
|
||||
|
||||
// ensure that it is clickable
|
||||
test('I can click on a tile in a frame and I get a modal after that', async function() {
|
||||
await expect(frame).toHaveElement(`.mosaic img`);
|
||||
await frame.click(`.mosaic img`);
|
||||
await frame.waitForSelector(".modal-content");
|
||||
});
|
||||
close();
|
||||
}, 6 * 60 * 1000); //give it up to 1 min to execute
|
||||
});
|
||||
async function waitForSelector(page, selector) {
|
||||
await page.waitForFunction(`document.querySelector('${selector}') && document.querySelector('${selector}').clientHeight != 0`);
|
||||
}
|
||||
|
||||
function mainTest() {
|
||||
describe("Main test", () => {
|
||||
describe("I visit a main page and have all required elements", () => {
|
||||
test('I can open a page', async function() {
|
||||
page = await makePage(appUrl + '/card-mode');
|
||||
await page.waitForSelector('.cards-section');
|
||||
});
|
||||
|
||||
//header
|
||||
test('A proper header is present', async function() {
|
||||
await expect(page).toHaveElement(`//h1[text() = '${settings.test.header}']`);
|
||||
});
|
||||
test('Group headers are ok', async function() {
|
||||
await expect(page).toHaveElement(`//a[contains(text(), '${settings.test.section}')]`);
|
||||
});
|
||||
test('I see a You are viewing text', async function() {
|
||||
await expect(page).toHaveElement(`//*[contains(text(), 'You are viewing ')]`);
|
||||
});
|
||||
test(`A proper card is present`, async function() {
|
||||
await expect(page).toHaveElement(`.mosaic img[src='${pathPrefix}/logos/${settings.test.logo}']`);
|
||||
});
|
||||
test(`If I click on a card, I see a modal dialog`, async function() {
|
||||
await page.click(`.mosaic img[src='${pathPrefix}/logos/${settings.test.logo}']`);
|
||||
await page.waitForSelector(".modal-content");
|
||||
});
|
||||
close();
|
||||
}, 6 * 60 * 1000); //give it up to 1 min to execute
|
||||
});
|
||||
async function waitForSummaryText(page, text) {
|
||||
await page.waitForFunction(`document.querySelector('.summary') && document.querySelector('.summary').innerText.includes('${text}')`);
|
||||
}
|
||||
|
||||
function landscapeTest() {
|
||||
describe("Big Picture Test", () => {
|
||||
describe("I visit a main landscape page and have all required elements", () => {
|
||||
test('I open a landscape page and wait for it to load', async function() {
|
||||
page = await makePage(appUrl);
|
||||
await page.waitForSelector('.big-picture-section');
|
||||
});
|
||||
test('When I click on an item the modal is open', async function() {
|
||||
await page.click('.big-picture-section img[src]');
|
||||
await page.waitForSelector(".modal-content");
|
||||
});
|
||||
async function waitForHeaderText(page, text) {
|
||||
await page.waitForFunction(`[...document.querySelectorAll('.sh_wrapper')].find( (x) => x.innerText.includes('${text}'))`);
|
||||
}
|
||||
|
||||
// and check that without redirect it works too
|
||||
test('If I would straight open the url with a selected id, a modal appears', async function() {
|
||||
await page.goto(appUrl);
|
||||
await page.waitForSelector('.big-picture-section');
|
||||
await page.click('.big-picture-section img[src]');
|
||||
await page.waitForSelector(".modal-content");
|
||||
});
|
||||
close();
|
||||
}, 6 * 60 * 1000); //give it up to 1 min to execute
|
||||
});
|
||||
landscapeSettingsList.slice(1).forEach(({ name, basePath }) => {
|
||||
// describe("Embed test", () => {
|
||||
// describe("I visit an example embed page", () => {
|
||||
// let frame;
|
||||
// test('page is open and has a frame', async function(){
|
||||
// page = await makePage(appUrl + '/embed');
|
||||
// frame = await page.frames()[1];
|
||||
// await frame.waitForSelector('.cards-section .mosaic');
|
||||
// await waitForSelector(frame, '#embedded-footer');
|
||||
// });
|
||||
|
||||
// test('Do not see a content from a main mode', async function() {
|
||||
// const title = await frame.$('h1', { text: settings.test.header })
|
||||
// expect(await title.boundingBox()).toBe(null)
|
||||
// });
|
||||
|
||||
// // ensure that it is clickable
|
||||
// test('I can click on a tile in a frame and I get a modal after that', async function() {
|
||||
// await waitForSelector(frame, ".cards-section .mosaic img");
|
||||
// await frame.click(`.mosaic img`);
|
||||
// });
|
||||
// close();
|
||||
// }, 6 * 60 * 1000); //give it up to 1 min to execute
|
||||
// });
|
||||
|
||||
describe("Main test", () => {
|
||||
describe("I visit a main page and have all required elements", () => {
|
||||
test('I can open a page', async function() {
|
||||
page = await makePage(appUrl + '/card-mode');
|
||||
await page.waitForSelector('.cards-section .mosaic');
|
||||
});
|
||||
|
||||
//header
|
||||
test('A proper header is present', async function() {
|
||||
await expect(page).toHaveElement(`//h1[text() = '${settings.test.header}']`);
|
||||
});
|
||||
|
||||
test('Group headers are ok', async function() {
|
||||
await waitForHeaderText(page, settings.test.section);
|
||||
});
|
||||
|
||||
test('I see a You are viewing text', async function() {
|
||||
await waitForSummaryText(page, 'You are viewing ');
|
||||
});
|
||||
|
||||
test(`A proper card is present`, async function() {
|
||||
await expect(page).toHaveElement(`.mosaic img[src='${pathPrefix}/logos/${settings.test.logo}']`);
|
||||
});
|
||||
|
||||
test(`If I click on a card, I see a modal dialog`, async function() {
|
||||
await page.click(`.mosaic img[src='${pathPrefix}/logos/${settings.test.logo}']`);
|
||||
await waitForSelector(page, ".modal-content .product-logo");
|
||||
});
|
||||
close();
|
||||
}, 6 * 60 * 1000); //give it up to 1 min to execute
|
||||
});
|
||||
|
||||
describe("Landscape Test", () => {
|
||||
describe("I visit a main landscape page and have all required elements", () => {
|
||||
test('I open a landscape page and wait for it to load', async function() {
|
||||
page = await makePage(appUrl);
|
||||
await page.waitForSelector('.cards-section [data-mode=main]');
|
||||
});
|
||||
test('When I click on an item the modal is open', async function() {
|
||||
await waitForSelector(page, '.cards-section [data-mode=main] [data-id]');
|
||||
await page.click('.cards-section [data-mode=main] [data-id]');
|
||||
await waitForSelector(page, ".modal-content .product-logo");
|
||||
});
|
||||
|
||||
test('If I would straight open the url with a selected id, a modal appears', async function() {
|
||||
await page.goto(appUrl);
|
||||
await waitForSelector(page, '.cards-section [data-mode=main] [data-id]');
|
||||
await page.click('.cards-section [data-mode=main] [data-id]');
|
||||
await waitForSelector(page, ".modal-content .product-logo");
|
||||
});
|
||||
close();
|
||||
}, 6 * 60 * 1000); //give it up to 1 min to execute
|
||||
landscapeSettingsList.slice(1).forEach(({ name, basePath, url }) => {
|
||||
test(`I visit ${name} landscape page and have all required elements, elements are clickable`, async () => {
|
||||
const page = await makePage(`${appUrl}/${basePath}`);
|
||||
await page.waitForSelector('.big-picture-section');
|
||||
await page.click('.big-picture-section img[src]');
|
||||
await page.waitForSelector(".modal-content");
|
||||
await waitForSelector(page, `.cards-section [data-mode=${url}] [data-id]`);
|
||||
await page.click(`.cards-section [data-mode=${url}] [data-id]`);
|
||||
await waitForSelector(page, ".modal-content .product-logo");
|
||||
}, 6 * 60 * 1000); //give it up to 1 min to execute
|
||||
close();
|
||||
})
|
||||
}
|
||||
|
||||
describe("Normal browser", function() {
|
||||
beforeAll(async function() {
|
||||
setup = async (page) => await page.setViewport({ width, height });
|
||||
})
|
||||
mainTest();
|
||||
landscapeTest();
|
||||
embedTest();
|
||||
|
||||
describe("Filtering by organization", () => {
|
||||
const project = projects[0];
|
||||
const organizationSlug = paramCase(project.organization);
|
||||
const otherProject = projects.find(({ organization }) => organization.toLowerCase() !== project.organization.toLowerCase());
|
||||
if (otherProject) {
|
||||
const otherOrganizationSlug = paramCase(otherProject.organization);
|
||||
|
||||
test(`Checking we see ${project.name} when filtering by organization ${project.organization}`, async function() {
|
||||
page = await makePage(`${appUrl}/card-mode?organization=${organizationSlug}`);
|
||||
await expect(page).toHaveElement(`//div[contains(@class, 'mosaic')]//*[text()='${project.name}']`);
|
||||
});
|
||||
test(`Checking we don't see ${project.name} when filtering by organization ${otherProject.organization}`, async function() {
|
||||
await page.goto(`${appUrl}/card-mode/organization=${otherOrganizationSlug}`);
|
||||
await expect(page).not.toHaveElement(`//div[contains(@class, 'mosaic')]//*[text()='${project.name}']`);
|
||||
});
|
||||
}
|
||||
close();
|
||||
}, 6 * 60 * 1000);
|
||||
});
|
||||
|
||||
describe("iPhone simulator", function() {
|
||||
beforeAll(async function() {
|
||||
setup = async (page) => await page.emulate(devicesMap['iPhone X'])
|
||||
})
|
||||
mainTest();
|
||||
landscapeTest();
|
||||
});
|
||||
describe("Filtering by organization", () => {
|
||||
const project = projects[0];
|
||||
const organizationSlug = paramCase(project.organization);
|
||||
const otherProject = projects.find(({ organization }) => organization.toLowerCase() !== project.organization.toLowerCase());
|
||||
if (otherProject) {
|
||||
const otherOrganizationSlug = paramCase(otherProject.organization);
|
||||
|
||||
test(`Checking we see ${project.name} when filtering by organization ${project.organization}`, async function() {
|
||||
page = await makePage(`${appUrl}/card-mode?organization=${organizationSlug}`);
|
||||
await page.waitForSelector('.cards-section .mosaic');
|
||||
await expect(page).toHaveElement(`//div[contains(@class, 'mosaic')]//*[text()='${project.name}']`);
|
||||
});
|
||||
test(`Checking we don't see ${project.name} when filtering by organization ${otherProject.organization}`, async function() {
|
||||
await page.goto(`${appUrl}/card-mode?organization=${otherOrganizationSlug}`);
|
||||
await page.waitForSelector('.cards-section .mosaic');
|
||||
await expect(page).not.toHaveElement(`//div[contains(@class, 'mosaic')]//*[text()='${project.name}']`);
|
||||
});
|
||||
}
|
||||
close();
|
||||
}, 6 * 60 * 1000);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import actualTwitter from '../../tools/actualTwitter';
|
||||
const { actualTwitter } = require('../../tools/actualTwitter');
|
||||
|
||||
describe('Twitter URL', () => {
|
||||
describe('when crunchbase data not set', () => {
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
import React from 'react'
|
||||
import settings from 'public/settings.json'
|
||||
import OutboundLink from './components/OutboundLink'
|
||||
import assetPath from './utils/assetPath'
|
||||
|
||||
const OrganizationLogo = () => {
|
||||
const { short_name, company_url } = settings.global
|
||||
|
||||
return <OutboundLink eventLabel={short_name} to={company_url} className="landscapeapp-logo" title={`${short_name} Home`}>
|
||||
<img src={assetPath("/images/right-logo.svg")} title={`${short_name} Logo`}/>
|
||||
</OutboundLink>
|
||||
}
|
||||
|
||||
export default OrganizationLogo
|
|
@ -0,0 +1,49 @@
|
|||
const { flattenItems } = require('../utils/itemsCalculator');
|
||||
const { getGroupedItems, expandSecondPathItems } = require('../utils/itemsCalculator');
|
||||
const { parseParams } = require('../utils/routing');
|
||||
const Parser = require('json2csv/lib/JSON2CSVParser');
|
||||
const { readJsonFromDist } = require('../utils/readJson');
|
||||
|
||||
const allItems = readJsonFromDist('data/items-export');
|
||||
const projects = readJsonFromDist('data/items');
|
||||
|
||||
const processRequest = module.exports.processRequest = query => {
|
||||
const params = parseParams(query);
|
||||
const p = new URLSearchParams(query);
|
||||
params.format = p.get('format');
|
||||
|
||||
let items = projects;
|
||||
if (params.grouping === 'landscape' || params.format !== 'card') {
|
||||
items = expandSecondPathItems(items);
|
||||
}
|
||||
|
||||
// extract alias - if grouping = category
|
||||
// extract alias - if params != card-mode (big_picture - always show)
|
||||
// i.e. make a copy to items here - to get a list of ids
|
||||
|
||||
const selectedItems = flattenItems(getGroupedItems({data: items, skipDuplicates: params.format === 'card', ...params}))
|
||||
.reduce((acc, item) => ({ ...acc, [item.id]: true }), {})
|
||||
|
||||
const fields = allItems[0].map(([label]) => label !== 'id' && label).filter(_ => _);
|
||||
const itemsForExport = allItems
|
||||
.map(item => item.reduce((acc, [label, value]) => ({ ...acc, [label]: value }), {}))
|
||||
.filter(item => selectedItems[item.id]);
|
||||
|
||||
const json2csvParser = new Parser({ fields });
|
||||
const csv = json2csvParser.parse(itemsForExport, { fields });
|
||||
return csv;
|
||||
}
|
||||
|
||||
// Netlify function
|
||||
module.exports.handler = async function(event) {
|
||||
const body = processRequest(event.queryStringParameters)
|
||||
const headers = {
|
||||
'Content-Type': 'text/css',
|
||||
'Content-Disposition': 'attachment; filename=interactive-landscape.csv'
|
||||
};
|
||||
return { statusCode: 200, body: body, headers }
|
||||
}
|
||||
if (__filename === process.argv[1]) {
|
||||
console.info(processRequest(process.argv[2]), null, 4);
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
const { expandSecondPathItems } = require('../utils/itemsCalculator');
|
||||
const { getGroupedItems } = require('../utils/itemsCalculator');
|
||||
const { getSummary, getSummaryText } = require('../utils/summaryCalculator');
|
||||
const { parseParams } = require('../utils/routing');
|
||||
const { readJsonFromDist } = require('../utils/readJson');
|
||||
|
||||
const projects = readJsonFromDist('data/items');
|
||||
|
||||
const processRequest = module.exports.processRequest = query => {
|
||||
const params = parseParams(query);
|
||||
const p = new URLSearchParams(query);
|
||||
params.format = p.get('format');
|
||||
|
||||
let items = projects;
|
||||
if (params.grouping === 'landscape' || params.format !== 'card') {
|
||||
items = expandSecondPathItems(items);
|
||||
}
|
||||
|
||||
const summary = getSummary({data: items, ...params});
|
||||
const groupedItems = getGroupedItems({data: items, skipDuplicates: params.format === 'card', ...params })
|
||||
.map(group => {
|
||||
const items = group.items.map(({ id }) => ({ id } ))
|
||||
return { ...group, items }
|
||||
})
|
||||
|
||||
return {
|
||||
summaryText: getSummaryText(summary),
|
||||
items: groupedItems
|
||||
}
|
||||
}
|
||||
|
||||
// Netlify function
|
||||
module.exports.handler = async function(event) {
|
||||
const body = processRequest(event.queryStringParameters)
|
||||
const headers = { 'Content-Type': 'application/json' }
|
||||
return { statusCode: 200, body: JSON.stringify(body), headers }
|
||||
}
|
||||
|
||||
if (__filename === process.argv[1]) {
|
||||
console.info(JSON.stringify(processRequest(process.argv[2]), null, 4));
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
const { flattenItems, expandSecondPathItems } = require('../utils/itemsCalculator');
|
||||
const { getGroupedItems } = require('../utils/itemsCalculator');
|
||||
const { parseParams } = require('../utils/routing');
|
||||
const { readJsonFromDist } = require('../utils/readJson');
|
||||
|
||||
const projects = readJsonFromDist('data/items');
|
||||
const settings = readJsonFromDist('settings');
|
||||
|
||||
const processRequest = module.exports.processRequest = query => {
|
||||
const params = parseParams({ mainContentMode: 'card-mode', ...query })
|
||||
// extract alias - if grouping = category
|
||||
// extract alias - if params != card-mode (big_picture - always show)
|
||||
// i.e. make a copy to items here - to get a list of ids
|
||||
let items = projects;
|
||||
if (params.grouping === 'landscape') {
|
||||
items = expandSecondPathItems(items);
|
||||
}
|
||||
|
||||
const groupedItems = getGroupedItems({data: items, ...params, skipDuplicates: true})
|
||||
.map(group => {
|
||||
const items = group.items.map(({ id, name, href }) => ({ id, name, logo: `${settings.global.website}/${href}` }))
|
||||
return { ...group, items }
|
||||
})
|
||||
return params.grouping === 'no' ? flattenItems(groupedItems) : groupedItems
|
||||
}
|
||||
// Netlify function
|
||||
module.exports.handler = async function(event) {
|
||||
const body = processRequest(event.queryStringParameters)
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Credentials': true
|
||||
}
|
||||
return { statusCode: 200, body: JSON.stringify(body), headers }
|
||||
}
|
|
@ -1,263 +0,0 @@
|
|||
import React, { useState } from 'react'
|
||||
import Container from '@material-ui/core/Container'
|
||||
import Table from '@material-ui/core/Table'
|
||||
import TableBody from '@material-ui/core/TableBody'
|
||||
import TableCell from '@material-ui/core/TableCell'
|
||||
import TableContainer from '@material-ui/core/TableContainer'
|
||||
import TableHead from '@material-ui/core/TableHead'
|
||||
import TablePagination from '@material-ui/core/TablePagination'
|
||||
import TableRow from '@material-ui/core/TableRow'
|
||||
import Paper from '@material-ui/core/Paper'
|
||||
import Typography from "@material-ui/core/Typography"
|
||||
import Box from "@material-ui/core/Box"
|
||||
import Toolbar from "@material-ui/core/Toolbar"
|
||||
import TextField from "@material-ui/core/TextField"
|
||||
import FormControl from "@material-ui/core/FormControl"
|
||||
import InputLabel from "@material-ui/core/InputLabel"
|
||||
import Select from "@material-ui/core/Select"
|
||||
import MenuItem from "@material-ui/core/MenuItem"
|
||||
import Button from "@material-ui/core/Button"
|
||||
import Dialog from "@material-ui/core/Dialog"
|
||||
import DialogTitle from "@material-ui/core/DialogTitle"
|
||||
import DialogContent from "@material-ui/core/DialogContent"
|
||||
import DialogActions from "@material-ui/core/DialogActions"
|
||||
import FilterListIcon from "@material-ui/icons/FilterList"
|
||||
import Autocomplete from "@material-ui/lab/Autocomplete"
|
||||
import { DatePicker, MuiPickersUtilsProvider } from '@material-ui/pickers'
|
||||
import DateFnsUtils from '@date-io/date-fns'
|
||||
import millify from 'millify'
|
||||
import OutboundLink from './OutboundLink'
|
||||
import { sortBy, uniq } from 'lodash'
|
||||
import { stringifyParams } from '../utils/routing'
|
||||
|
||||
const makeOptions = (arr) => sortBy(uniq(arr), name => name.toLowerCase())
|
||||
|
||||
const Acquistions = ({ ...props }) => {
|
||||
const acquisitions = props.acquisitions.map(data => ({ ...data, date: new Date(data.date) }))
|
||||
.sort((a, b) => b.date - a.date)
|
||||
const members = new Set(props.members)
|
||||
const acquirers = makeOptions(acquisitions.map(a => a.acquirer))
|
||||
const acquirees = makeOptions(acquisitions.map(a => a.acquiree).filter(a => a))
|
||||
|
||||
const linkToOrg = organization => {
|
||||
if (!members.has(organization)) {
|
||||
return organization
|
||||
}
|
||||
const url = stringifyParams({ mainContentMode: 'landscape', filters: { organization }})
|
||||
return <OutboundLink to={url}>{organization}</OutboundLink>
|
||||
}
|
||||
|
||||
const rowKey = ({ acquirer, acquiree, date }) => {
|
||||
return `${acquirer}-${acquiree}-${date.toString()}`
|
||||
}
|
||||
|
||||
const rowsPerPage = 20
|
||||
const [page, setPage] = useState(0)
|
||||
const [dialogOpen, setDialogOpen] = useState(false)
|
||||
const closeDialog = () => setDialogOpen(false)
|
||||
const defaultFilters = {
|
||||
acquirers: [],
|
||||
acquirees: [],
|
||||
min_date: null,
|
||||
max_date: null,
|
||||
min_price: '',
|
||||
max_price: ''
|
||||
}
|
||||
const priceOptions = [
|
||||
{ label: '', value: '' },
|
||||
{ label: '$100M', value: 100000000 },
|
||||
{ label: '$250M', value: 250000000 },
|
||||
{ label: '$500M', value: 500000000 },
|
||||
{ label: '$1B', value: 1000000000 },
|
||||
{ label: '$5B', value: 5000000000 },
|
||||
{ label: '$10B', value: 10000000000 },
|
||||
{ label: '$20B', value: 20000000000 },
|
||||
{ label: '$50B', value: 50000000000 },
|
||||
{ label: '$100B', value: 100000000000 }
|
||||
]
|
||||
const [filters, setFilters] = useState(defaultFilters)
|
||||
const filterFn = (acquisition) => {
|
||||
if (filters.acquirers.length > 0 && !filters.acquirers.includes(acquisition.acquirer)) {
|
||||
return false
|
||||
}
|
||||
if (filters.acquirees.length > 0 && !filters.acquirees.includes(acquisition.acquiree)) {
|
||||
return false
|
||||
}
|
||||
if (filters.min_date && filters.min_date > acquisition.date) {
|
||||
return false
|
||||
}
|
||||
if (filters.max_date && filters.max_date < acquisition.date) {
|
||||
return false
|
||||
}
|
||||
if (filters.min_price && (!acquisition.price || filters.min_price > acquisition.price)) {
|
||||
return false
|
||||
}
|
||||
if (filters.max_price && (!acquisition.price || filters.max_price < acquisition.price)) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const resetFilters = () => {
|
||||
setPage(0)
|
||||
setFilters(defaultFilters)
|
||||
}
|
||||
const setFilter = (name, value) => {
|
||||
setPage(0)
|
||||
setFilters({ ...filters, [name]: value })
|
||||
}
|
||||
|
||||
const hasFilters = () => {
|
||||
const { acquirers, acquirees, min_date, max_date, min_price, max_price } = filters
|
||||
return acquirers.length > 0 || acquirees.length > 0 || min_date || max_date || min_price || max_price
|
||||
}
|
||||
|
||||
const filteredAcquisitions = acquisitions.filter(filterFn)
|
||||
|
||||
const data = filteredAcquisitions.slice(page * rowsPerPage, (page + 1) * rowsPerPage)
|
||||
|
||||
const filterDialog = () => {
|
||||
return <Dialog onClose={closeDialog} open={dialogOpen} maxWidth='sm' fullWidth={true}>
|
||||
<DialogTitle>Filter Acquisitions</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box mb={3}>
|
||||
<Autocomplete
|
||||
multiple
|
||||
options={acquirers}
|
||||
value={filters.acquirers}
|
||||
renderInput={params => <TextField {...params} label="Acquirer"/>}
|
||||
onChange={(_, value) => setFilter('acquirers', value)}
|
||||
/>
|
||||
</Box>
|
||||
<Box mb={3}>
|
||||
<Autocomplete
|
||||
multiple
|
||||
options={acquirees}
|
||||
value={filters.acquirees}
|
||||
renderInput={params => <TextField {...params} label="Acquiree" />}
|
||||
onChange={(_, value) => setFilter('acquirees', value)}
|
||||
/>
|
||||
</Box>
|
||||
<MuiPickersUtilsProvider utils={DateFnsUtils}>
|
||||
<Box mb={3} flexDirection="row" display={"flex"}>
|
||||
<Box mr={2} flexGrow="1">
|
||||
<DatePicker
|
||||
value={filters.min_date}
|
||||
onChange={value => setFilter('min_date', value)}
|
||||
label="From Date"
|
||||
autoOk
|
||||
fullWidth
|
||||
disableFuture
|
||||
labelFunc={(date) => date ? date.toLocaleDateString() : ''}
|
||||
clearable={true}
|
||||
/>
|
||||
</Box>
|
||||
<Box ml={2} flexGrow="1">
|
||||
<DatePicker
|
||||
value={filters.max_date}
|
||||
onChange={value => setFilter('max_date', value)}
|
||||
label="To Date"
|
||||
autoOk
|
||||
fullWidth
|
||||
disableFuture
|
||||
labelFunc={(date) => date ? date.toLocaleDateString() : ''}
|
||||
clearable={true}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</MuiPickersUtilsProvider>
|
||||
<Box mb={5} flexDirection="row" display={"flex"}>
|
||||
<Box mr={2} flexGrow="1">
|
||||
<FormControl fullWidth={true}>
|
||||
<InputLabel>Min Price</InputLabel>
|
||||
<Select
|
||||
value={filters.min_price}
|
||||
onChange={e => setFilter('min_price', e.target.value)}
|
||||
>
|
||||
{priceOptions.map(({ value, label }) => <MenuItem key={value} value={value}>{label}</MenuItem>)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
<Box ml={2} flexGrow="1">
|
||||
<FormControl fullWidth={true}>
|
||||
<InputLabel>Max Price</InputLabel>
|
||||
<Select
|
||||
value={filters.max_price}
|
||||
onChange={e => setFilter('max_price', e.target.value)}
|
||||
>
|
||||
{priceOptions.map(({ value, label }) => <MenuItem key={value} value={value}>{label}</MenuItem>)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={resetFilters} color="primary">
|
||||
Reset
|
||||
</Button>
|
||||
<Button onClick={closeDialog}>
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
}
|
||||
|
||||
return <Container maxWidth="lg" disableGutters={true}>
|
||||
<Paper>
|
||||
<Toolbar variant="dense">
|
||||
<Box flexGrow="1">
|
||||
<Typography variant="h6">
|
||||
Acquisitions
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{ hasFilters() ?
|
||||
<Button color="primary" onClick={resetFilters}>Reset</Button>
|
||||
: null
|
||||
}
|
||||
|
||||
<Button
|
||||
aria-label="filter list"
|
||||
onClick={() => setDialogOpen(true)}
|
||||
endIcon={<FilterListIcon>filter</FilterListIcon>}
|
||||
>
|
||||
Filter
|
||||
</Button>
|
||||
|
||||
{filterDialog()}
|
||||
</Toolbar>
|
||||
<TableContainer>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Acquirer</TableCell>
|
||||
<TableCell>Acquiree</TableCell>
|
||||
<TableCell align="right">Price</TableCell>
|
||||
<TableCell align="right">Date</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{data.map(acquisition => {
|
||||
return <TableRow key={rowKey(acquisition)}>
|
||||
<TableCell>{linkToOrg(acquisition.acquirer)}</TableCell>
|
||||
<TableCell>{acquisition.acquiree && linkToOrg(acquisition.acquiree)}</TableCell>
|
||||
<TableCell align="right">{acquisition.price && `$${millify(acquisition.price)}`}</TableCell>
|
||||
<TableCell align="right">{acquisition.date.toLocaleDateString()}</TableCell>
|
||||
</TableRow>
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<TablePagination
|
||||
component="div"
|
||||
count={filteredAcquisitions.length}
|
||||
rowsPerPage={rowsPerPage}
|
||||
page={page}
|
||||
rowsPerPageOptions={[]}
|
||||
onChangePage={(_, page) => setPage(page)}
|
||||
/>
|
||||
</Paper>
|
||||
</Container>
|
||||
}
|
||||
|
||||
export default Acquistions
|
|
@ -1,18 +0,0 @@
|
|||
import React from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import OutboundLink from './OutboundLink';
|
||||
import settings from 'public/settings.json';
|
||||
import assetPath from '../utils/assetPath'
|
||||
|
||||
const Ad = () => {
|
||||
const entries = settings.ads || [];
|
||||
|
||||
return <div id="kubecon">
|
||||
{ entries.map( (entry) => (
|
||||
<OutboundLink className="sidebar-event" key={entry.image} to={entry.url} title={entry.title}>
|
||||
<img src={assetPath(entry.image)} alt={entry.title} />
|
||||
</OutboundLink>
|
||||
)) }
|
||||
</div>
|
||||
}
|
||||
export default pure(Ad);
|
|
@ -1,14 +0,0 @@
|
|||
import { useContext } from 'react'
|
||||
import ComboboxMultiSelector from './ComboboxMultiSelector'
|
||||
import { options } from '../types/fields';
|
||||
import LandscapeContext from '../contexts/LandscapeContext'
|
||||
|
||||
const ArrayFilterContainer = ({ name }) => {
|
||||
const { navigate, params } = useContext(LandscapeContext)
|
||||
const value = params.filters[name]
|
||||
const _options = options(name)
|
||||
const onChange = value => navigate({ filters: { [name]: value } })
|
||||
return <ComboboxMultiSelector onChange={onChange} value={value} options={_options} />
|
||||
}
|
||||
|
||||
export default ArrayFilterContainer
|
|
@ -1,30 +0,0 @@
|
|||
// locate zoom buttons
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import { pure } from 'recompose';
|
||||
import FullscreenExitIcon from '@material-ui/icons/FullscreenExit';
|
||||
import FullscreenIcon from '@material-ui/icons/Fullscreen';
|
||||
|
||||
import React, { useContext } from 'react';
|
||||
import LandscapeContext from '../../contexts/LandscapeContext'
|
||||
|
||||
const FullscreenButton = _ => {
|
||||
const { navigate, params } = useContext(LandscapeContext)
|
||||
const isBigPicture = params.mainContentMode !== 'card-mode'
|
||||
const { isFullscreen } = params
|
||||
|
||||
if (!isBigPicture) {
|
||||
return null;
|
||||
}
|
||||
return <div className="fullscreen-button">
|
||||
{ isFullscreen ?
|
||||
<IconButton onClick={_ => navigate({ isFullscreen: false })} title="Exit fullscreen" size="small">
|
||||
<FullscreenExitIcon />
|
||||
</IconButton>
|
||||
:
|
||||
<IconButton onClick={_ => navigate({ isFullscreen: true })} title="Enter fullscreen" size="small">
|
||||
<FullscreenIcon />
|
||||
</IconButton>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
export default pure(FullscreenButton);
|
|
@ -1,142 +0,0 @@
|
|||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import LandscapeContent from './LandscapeContent';
|
||||
import useCurrentDevice from '../../utils/useCurrentDevice'
|
||||
import { useRouter } from 'next/router'
|
||||
import LandscapeContext from '../../contexts/LandscapeContext'
|
||||
import { headerHeight} from '../../utils/landscapeCalculations'
|
||||
|
||||
const _calculateZoom = (fullscreenWidth, fullscreenHeight, zoomedIn, currentDevice) => {
|
||||
const isFirefox = navigator.userAgent.indexOf('Firefox') > -1
|
||||
|
||||
const aspectRatio = innerWidth / innerHeight
|
||||
const adjustedWidth = outerWidth
|
||||
const adjustedHeight = adjustedWidth / aspectRatio
|
||||
let baseZoom = Math.min(adjustedHeight / fullscreenHeight, adjustedWidth / fullscreenWidth, 2).toPrecision(4)
|
||||
let wrapperWidth, wrapperHeight
|
||||
|
||||
if (isFirefox || location.search.indexOf('scale=false') > -1) {
|
||||
wrapperWidth = Math.max(fullscreenWidth, innerWidth)
|
||||
wrapperHeight = Math.max(fullscreenHeight, innerHeight)
|
||||
baseZoom = 1
|
||||
} else {
|
||||
wrapperWidth = adjustedWidth / baseZoom
|
||||
wrapperHeight = adjustedHeight / baseZoom
|
||||
}
|
||||
|
||||
return { zoom: Math.min(baseZoom * (zoomedIn ? 3 : 1), 3), wrapperWidth, wrapperHeight }
|
||||
}
|
||||
|
||||
const Fullscreen = _ => {
|
||||
const { version } = useRouter().query
|
||||
const { fullscreenWidth, fullscreenHeight, landscapeSettings } = useContext(LandscapeContext)
|
||||
const currentDevice = useCurrentDevice()
|
||||
|
||||
const [zoomState, setZoomState] = useState({
|
||||
zoom: 1,
|
||||
wrapperHeight: fullscreenHeight,
|
||||
wrapperWidth: fullscreenWidth,
|
||||
zoomedIn: false,
|
||||
zoomedAt: {}
|
||||
})
|
||||
const { zoom, wrapperHeight, wrapperWidth, zoomedIn, zoomedAt } = zoomState
|
||||
|
||||
const calculateZoom = (zoomedIn = false, zoomedAt = {}) => {
|
||||
const zoomAttrs = _calculateZoom(fullscreenWidth, fullscreenHeight, zoomedIn, currentDevice)
|
||||
setZoomState({ zoomedIn, zoomedAt, ...zoomAttrs })
|
||||
}
|
||||
|
||||
const onZoom = e => {
|
||||
const zoomedAt = { x: e.pageX / zoom, y: e.pageY / zoom }
|
||||
calculateZoom(!zoomedIn, zoomedAt)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (currentDevice.ready && currentDevice.desktop()) {
|
||||
calculateZoom()
|
||||
const onResize = _ => calculateZoom()
|
||||
window.addEventListener('resize', onResize)
|
||||
return () => window.removeEventListener('resize', onResize)
|
||||
}
|
||||
}, [currentDevice]);
|
||||
|
||||
useEffect(() => {
|
||||
window.scrollTo((zoomedAt.x * zoom - innerWidth / 2), (zoomedAt.y * zoom - innerHeight / 2))
|
||||
}, [zoomedAt, zoomedIn])
|
||||
|
||||
return (
|
||||
<div className="gradient-bg" style={{
|
||||
width: wrapperWidth * zoom,
|
||||
height: wrapperHeight * zoom,
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
<div style={{
|
||||
transform: `scale(${zoom})`,
|
||||
width: wrapperWidth,
|
||||
height: wrapperHeight,
|
||||
transformOrigin: '0 0',
|
||||
paddingTop: headerHeight,
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<div
|
||||
onClick={e => currentDevice.desktop() && onZoom(e)}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
cursor: zoomedIn ? 'zoom-out' : 'zoom-in',
|
||||
zIndex: 100000
|
||||
}}>
|
||||
</div>
|
||||
<LandscapeContent padding={0} />
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: 10,
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
fontSize: 18,
|
||||
background: 'rgb(64,89,163)',
|
||||
color: 'white',
|
||||
paddingLeft: 20,
|
||||
paddingRight: 20,
|
||||
paddingTop: 3,
|
||||
paddingBottom: 3,
|
||||
borderRadius: 5
|
||||
}}>{landscapeSettings.fullscreen_header}</div>
|
||||
{ !landscapeSettings.fullscreen_hide_grey_logos && <div style={{
|
||||
position: 'absolute',
|
||||
top: 15,
|
||||
right: 12,
|
||||
fontSize: 11,
|
||||
background: '#eee',
|
||||
color: 'rgb(100,100,100)',
|
||||
paddingLeft: 20,
|
||||
paddingRight: 20,
|
||||
paddingTop: 3,
|
||||
paddingBottom: 3,
|
||||
borderRadius: 5
|
||||
}}>Greyed logos are not open source</div> }
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: 10,
|
||||
left: 15,
|
||||
fontSize: 14,
|
||||
color: 'white',
|
||||
}}>{landscapeSettings.title} </div>
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: 30,
|
||||
left: 15,
|
||||
fontSize: 12,
|
||||
color: '#eee',
|
||||
}}>{version}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Fullscreen
|
|
@ -1,120 +0,0 @@
|
|||
import React, { Fragment, useContext } from "react";
|
||||
import Item from "./Item";
|
||||
import InternalLink from "../InternalLink";
|
||||
import {
|
||||
calculateHorizontalCategory,
|
||||
categoryBorder,
|
||||
categoryTitleHeight,
|
||||
dividerWidth,
|
||||
itemMargin,
|
||||
smallItemWidth,
|
||||
smallItemHeight,
|
||||
subcategoryMargin,
|
||||
subcategoryTitleHeight
|
||||
} from "../../utils/landscapeCalculations";
|
||||
import LandscapeContext from '../../contexts/LandscapeContext'
|
||||
import SubcategoryInfo from '../SubcategoryInfo'
|
||||
import CategoryHeader from '../CategoryHeader'
|
||||
|
||||
const Divider = ({ color }) => {
|
||||
const width = dividerWidth
|
||||
const marginTop = 2 * subcategoryMargin
|
||||
const height = `calc(100% - ${2 * marginTop}px)`
|
||||
|
||||
return <div style={{ width, marginTop, height, borderLeft: `${width}px solid ${color}` }}/>
|
||||
}
|
||||
|
||||
const HorizontalCategory = ({ header, subcategories, width, height, top, left, color, href, fitWidth }) => {
|
||||
const { guideIndex } = useContext(LandscapeContext)
|
||||
const addInfoIcon = Object.keys(guideIndex).length > 0
|
||||
const subcategoriesWithCalculations = calculateHorizontalCategory({ height, width, subcategories, fitWidth, addInfoIcon })
|
||||
const totalRows = Math.max(...subcategoriesWithCalculations.map(({ rows }) => rows))
|
||||
|
||||
return (
|
||||
<div style={{ width, left, height, top, position: 'absolute' }} className="big-picture-section">
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
background: color,
|
||||
top: subcategoryTitleHeight,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
boxShadow: '0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.2)',
|
||||
padding: categoryBorder
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
width: categoryTitleHeight,
|
||||
position: 'absolute',
|
||||
writingMode: 'vertical-rl',
|
||||
transform: 'rotate(180deg)',
|
||||
textAlign: 'center',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<CategoryHeader href={href} label={header} guideAnchor={guideIndex[header]} background={color} rotate={true} />
|
||||
</div>
|
||||
<div style={{
|
||||
marginLeft: 30,
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-evenly',
|
||||
background: 'white'
|
||||
}}>
|
||||
{subcategoriesWithCalculations.map((subcategory, index) => {
|
||||
const lastSubcategory = index !== subcategories.length - 1
|
||||
const { allItems, columns, width, name, href } = subcategory
|
||||
const padding = fitWidth ? 0 : `${subcategoryMargin}px 0`
|
||||
const style = {
|
||||
display: 'grid',
|
||||
height: '100%',
|
||||
gridTemplateColumns: `repeat(${columns}, ${smallItemWidth}px)`,
|
||||
gridAutoRows: `${smallItemHeight}px`
|
||||
}
|
||||
const extraStyle = fitWidth ? { justifyContent: 'space-evenly', alignContent: 'space-evenly' } : { gridGap: itemMargin }
|
||||
const path = [header, name].join(' / ')
|
||||
|
||||
return <Fragment key={name}>
|
||||
<div style={{
|
||||
width,
|
||||
position: 'relative',
|
||||
overflow: 'visible',
|
||||
padding,
|
||||
boxSizing: 'border-box'
|
||||
}}>
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: -1 * categoryTitleHeight,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: categoryTitleHeight,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
<InternalLink to={href} className="white-link">{name}</InternalLink>
|
||||
</div>
|
||||
<div style={{...style, ...extraStyle}}>
|
||||
{
|
||||
allItems.map(item => <Item item={item} key={item.name}/>)
|
||||
}
|
||||
|
||||
{ guideIndex[path] && <SubcategoryInfo label={name} anchor={guideIndex[path]} column={columns} row={totalRows}/> }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{lastSubcategory && <Divider color={color}/>}
|
||||
</Fragment>
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
|
||||
export default HorizontalCategory
|
|
@ -1,97 +0,0 @@
|
|||
import { useContext } from 'react'
|
||||
import settings from 'public/settings.json'
|
||||
import fields from "../../types/fields";
|
||||
import {
|
||||
largeItemHeight,
|
||||
largeItemWidth,
|
||||
smallItemHeight,
|
||||
smallItemWidth
|
||||
} from "../../utils/landscapeCalculations";
|
||||
import LandscapeContext from '../../contexts/LandscapeContext'
|
||||
import assetPath from '../../utils/assetPath'
|
||||
|
||||
const LargeItem = ({ item, onClick }) => {
|
||||
const relationInfo = fields.relation.valuesMap[item.relation]
|
||||
const color = relationInfo.big_picture_color;
|
||||
const label = relationInfo.big_picture_label;
|
||||
const textHeight = label ? 10 : 0
|
||||
const padding = 2
|
||||
|
||||
return <div className="large-item item" onClick={onClick}>
|
||||
<style jsx>{`
|
||||
.large-item {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
background: ${color};
|
||||
visibility: ${item.isVisible ? 'visible' : 'hidden'};
|
||||
width: ${largeItemWidth}px;
|
||||
height: ${largeItemHeight}px;
|
||||
}
|
||||
|
||||
.large-item img {
|
||||
width: calc(100% - ${2 * padding}px);
|
||||
height: calc(100% - ${2 * padding + textHeight}px);
|
||||
padding: 5px;
|
||||
margin: ${padding}px ${padding}px 0 ${padding}px;
|
||||
}
|
||||
|
||||
.large-item .label {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: ${textHeight + padding}px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
background: ${color};
|
||||
color: white;
|
||||
font-size: 6.7px;
|
||||
line-height: 13px;
|
||||
}
|
||||
`}</style>
|
||||
|
||||
<img loading="lazy" src={assetPath(item.href)} data-href={item.id} alt={item.name} />
|
||||
<div className="label">{label}</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
const SmallItem = ({ item, onClick }) => {
|
||||
const isMember = item.category === settings.global.membership;
|
||||
return <>
|
||||
<style jsx>{`
|
||||
img {
|
||||
cursor: pointer;
|
||||
width: ${smallItemWidth}px;
|
||||
height: ${smallItemHeight}px;
|
||||
border: 1px solid ${isMember ? 'white' : 'grey'};
|
||||
border-radius: 2px;
|
||||
padding: 1px;
|
||||
visibility: ${item.isVisible ? 'visible' : 'hidden'};
|
||||
}
|
||||
`}</style>
|
||||
<img data-href={item.id} loading="lazy" className="item" src={assetPath(item.href)} onClick={onClick} alt={item.name} />
|
||||
</>
|
||||
}
|
||||
|
||||
const Item = props => {
|
||||
const { isLarge, category, oss, categoryAttrs } = props.item
|
||||
const isMember = category === settings.global.membership;
|
||||
const { navigate } = useContext(LandscapeContext)
|
||||
const onClick = _ => navigate({ selectedItemId: props.item.id }, { scroll: false })
|
||||
const newProps = { ...props, onClick }
|
||||
|
||||
return <div className={isMember || oss || categoryAttrs.isLarge ? 'oss' : 'nonoss'}>
|
||||
<style jsx>{`
|
||||
div {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
grid-column-end: span ${isLarge ? 2 : 1};
|
||||
grid-row-end: span ${isLarge ? 2 : 1};
|
||||
}
|
||||
`}</style>
|
||||
|
||||
{isLarge ? <LargeItem {...newProps} isMember={isMember} /> : <SmallItem {...newProps} />}
|
||||
</div>
|
||||
}
|
||||
|
||||
export default Item
|
|
@ -1,56 +0,0 @@
|
|||
import React, { useContext } from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import _ from 'lodash';
|
||||
import HorizontalCategory from './HorizontalCategory'
|
||||
import VerticalCategory from './VerticalCategory'
|
||||
import LandscapeInfo from './LandscapeInfo';
|
||||
import OtherLandscapeLink from './OtherLandscapeLink';
|
||||
import LandscapeContext from '../../contexts/LandscapeContext'
|
||||
|
||||
const extractKeys = (obj, keys) => {
|
||||
const attributes = _.pick(obj, keys)
|
||||
|
||||
return _.mapKeys(attributes, (value, key) => _.camelCase(key))
|
||||
}
|
||||
|
||||
const LandscapeContent = ({zoom, padding = 10 }) => {
|
||||
const { groupedItems, landscapeSettings, width, height } = useContext(LandscapeContext)
|
||||
const elements = landscapeSettings.elements.map(element => {
|
||||
if (element.type === 'LandscapeLink') {
|
||||
return <OtherLandscapeLink {..._.pick(element, ['width','height','top','left','color', 'layout', 'title', 'url', 'image']) }
|
||||
key={element.url}
|
||||
/>
|
||||
}
|
||||
if (element.type === 'LandscapeInfo') {
|
||||
return <LandscapeInfo {..._.pick(element, ['width', 'height', 'top', 'left']) } childrenInfo={element.children}
|
||||
key='landscape-info'
|
||||
/>
|
||||
}
|
||||
|
||||
const category = groupedItems.find(c => c.key === element.category) || {}
|
||||
const attributes = extractKeys(element, ['width', 'height', 'top', 'left', 'color', 'fit_width', 'is_large'])
|
||||
const subcategories = category.subcategories.map(subcategory => {
|
||||
const allItems = subcategory.allItems.map(item => ({ ...item, categoryAttrs: attributes }))
|
||||
return { ...subcategory, allItems }
|
||||
})
|
||||
|
||||
const Component = element.type === 'HorizontalCategory' ? HorizontalCategory : VerticalCategory
|
||||
return <Component {...category} subcategories={subcategories} {...attributes} />
|
||||
});
|
||||
|
||||
const style = {
|
||||
padding,
|
||||
width: width + 2 * padding,
|
||||
height: height + 2 * padding,
|
||||
transform: `scale(${zoom})`,
|
||||
transformOrigin: '0 0'
|
||||
}
|
||||
|
||||
return <div className="inner-landscape" style={style}>
|
||||
<div style={{ position: 'relative' }}>
|
||||
{elements}
|
||||
</div>
|
||||
</div>
|
||||
};
|
||||
|
||||
export default pure(LandscapeContent);
|
|
@ -1,76 +0,0 @@
|
|||
import React from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import _ from 'lodash';
|
||||
import assetPath from '../../utils/assetPath'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
const LandscapeInfo = ({width, height, top, left, childrenInfo}) => {
|
||||
const { query } = useRouter()
|
||||
const children = childrenInfo.map(function(info) {
|
||||
const positionProps = {
|
||||
position: 'absolute',
|
||||
top: _.isUndefined(info.top) ? null : info.top,
|
||||
left: _.isUndefined(info.left) ? null : info.left,
|
||||
right: _.isUndefined(info.right) ? null : info.right,
|
||||
bottom: _.isUndefined(info.bottom) ? null : info.bottom,
|
||||
width: _.isUndefined(info.width) ? null : info.width,
|
||||
height: _.isUndefined(info.height) ? null : info.height
|
||||
};
|
||||
if (info.type === 'text') {
|
||||
// pdf requires a normal version without a zoom trick
|
||||
if (query.hasOwnProperty('pdf')) {
|
||||
return <div key='text' style={{
|
||||
...positionProps,
|
||||
fontSize: info.font_size,
|
||||
fontStyle: 'italic',
|
||||
textAlign: 'justify',
|
||||
zIndex: 1
|
||||
}}>{info.text}</div>
|
||||
// while in a browser we use a special version which renders fonts
|
||||
// properly on a small zoom
|
||||
} else {
|
||||
return <div key='text' style={{
|
||||
...positionProps,
|
||||
fontSize: info.font_size * 4,
|
||||
fontStyle: 'italic',
|
||||
textAlign: 'justify',
|
||||
zIndex: 1
|
||||
}}><div style={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: '400%',
|
||||
height: '100%',
|
||||
transform: 'scale(0.25)',
|
||||
transformOrigin: 'left'
|
||||
}}> {info.text} </div></div>
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
if (info.type === 'title') {
|
||||
return <div key='title' style= {{
|
||||
...positionProps,
|
||||
fontSize: info.font_size,
|
||||
color: '#666'
|
||||
}}>{info.title}</div>
|
||||
}
|
||||
if (info.type === 'image') {
|
||||
return <img src={assetPath(`/images/${info.image}`)} style={{...positionProps}} key={info.image} alt={info.title || info.image} />
|
||||
}
|
||||
});
|
||||
|
||||
return <div style={{
|
||||
position: 'absolute',
|
||||
width: width,
|
||||
height: height - 20,
|
||||
top: top,
|
||||
left: left,
|
||||
border: '1px solid black',
|
||||
background: 'white',
|
||||
borderRadius: 10,
|
||||
marginTop: 20,
|
||||
boxShadow: `0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19)`
|
||||
}}>{children}</div>
|
||||
}
|
||||
export default pure(LandscapeInfo);
|
|
@ -1,74 +0,0 @@
|
|||
import React from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import assetPath from '../../utils/assetPath'
|
||||
import OutboundLink from '../OutboundLink'
|
||||
import InternalLink from '../InternalLink'
|
||||
import { stringifyParams } from '../../utils/routing'
|
||||
import { categoryBorder, categoryTitleHeight, subcategoryTitleHeight } from '../../utils/landscapeCalculations'
|
||||
|
||||
const CardLink = ({ url, children }) => {
|
||||
const Component = url.indexOf('http') === 0 ? OutboundLink : InternalLink
|
||||
const to = url.indexOf('http') === 0 ? url : stringifyParams({ mainContentMode: url })
|
||||
|
||||
return <Component to={to} style={{ display: 'flex', flexDirection: 'column' }}>{children}</Component>
|
||||
}
|
||||
|
||||
const OtherLandscapeLink = function({top, left, height, width, color, title, image, url, layout}) {
|
||||
const imageSrc = image || assetPath(`/images/${url}_preview.png`)
|
||||
if (layout === 'category') {
|
||||
return <div style={{
|
||||
position: 'absolute', top, left, height, width, background: color,
|
||||
cursor: 'pointer',
|
||||
boxShadow: `0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19)`,
|
||||
padding: 1,
|
||||
display: 'flex'
|
||||
}}>
|
||||
<CardLink url={url}>
|
||||
<div style={{ width, height: 30, lineHeight: '28px', textAlign: 'center', color: 'white', fontSize: 12}}>{title}</div>
|
||||
<div style={{ flex: 1, background: 'white', position: 'relative', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
|
||||
<img loading="lazy" src={imageSrc} style={{ width: width - 12, height: height - 42,
|
||||
objectFit: 'contain', backgroundPosition: 'center', backgroundRepeat: 'no-repeat' }} alt={title} />
|
||||
</div>
|
||||
</CardLink>
|
||||
</div>
|
||||
}
|
||||
if (layout === 'subcategory') {
|
||||
return <div style={{ width, left, height, top, position: 'absolute' }}>
|
||||
<CardLink url={url}>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
background: color,
|
||||
top: subcategoryTitleHeight,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
boxShadow: '0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.2)',
|
||||
padding: categoryBorder,
|
||||
display: 'flex'
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
width: categoryTitleHeight,
|
||||
writingMode: 'vertical-rl',
|
||||
transform: 'rotate(180deg)',
|
||||
textAlign: 'center',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: 12,
|
||||
lineHeight: '13px',
|
||||
color: 'white'
|
||||
}}>
|
||||
{title}
|
||||
</div>
|
||||
<div style={{ display: 'flex', flex: 1, background: 'white', justifyContent: 'center', alignItems: 'center' }}>
|
||||
<img loading="lazy" src={imageSrc} alt={title}
|
||||
style={{ width: width - 42, height: height - 32, objectFit: 'contain', backgroundPosition: 'center', backgroundRepeat: 'no-repeat' }} />
|
||||
</div>
|
||||
</div>
|
||||
</CardLink>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
export default pure(OtherLandscapeLink);
|
|
@ -1,48 +0,0 @@
|
|||
import React, { useContext } from 'react';
|
||||
import { pure, withProps, toClass } from 'recompose';
|
||||
import Tabs from '@material-ui/core/Tabs';
|
||||
import Tab from '@material-ui/core/Tab';
|
||||
import InternalLink from '../InternalLink';
|
||||
import LandscapeContext from '../../contexts/LandscapeContext'
|
||||
import _ from 'lodash'
|
||||
import settings from 'public/settings.json';
|
||||
import { stringifyParams } from '../../utils/routing'
|
||||
|
||||
const mainCard = [{shortTitle: 'Card', title: 'Card Mode', mode: 'card-mode', tabIndex: 0}]
|
||||
|
||||
const landscapes = _.map(settings.big_picture, function(section) {
|
||||
return {
|
||||
title: section.name,
|
||||
shortTitle: section.short_name,
|
||||
mode: section.url,
|
||||
tabIndex: section.tab_index
|
||||
}
|
||||
})
|
||||
|
||||
const _cards = _.orderBy(mainCard.concat(landscapes), 'tabIndex').map( item => _.pick(item, ['title', 'mode', 'shortTitle']))
|
||||
|
||||
const SwitchButton = _ => {
|
||||
const { navigate, params } = useContext(LandscapeContext)
|
||||
const { mainContentMode, isEmbed } = params
|
||||
const cards = _cards.map(card => ({ ...card, url: stringifyParams({ ...params, mainContentMode: card.mode })}))
|
||||
|
||||
if (isEmbed) {
|
||||
return null;
|
||||
}
|
||||
return [
|
||||
<Tabs
|
||||
className="big-picture-switch big-picture-switch-normal"
|
||||
value={mainContentMode}
|
||||
onChange={(_event, mainContentMode) => navigate({ mainContentMode })}
|
||||
key='tabs'
|
||||
>
|
||||
{ cards.map(({ mode, title, url}) => {
|
||||
const link = toClass(withProps(props => { return { to: url } })(InternalLink));
|
||||
return <Tab key={mode} label={title} component={link} value={mode} />
|
||||
})}
|
||||
</Tabs>
|
||||
]
|
||||
|
||||
|
||||
}
|
||||
export default pure(SwitchButton);
|
|
@ -1,53 +0,0 @@
|
|||
import React, { useContext } from "react";
|
||||
import Item from "./Item";
|
||||
import InternalLink from "../InternalLink";
|
||||
import {
|
||||
calculateVerticalCategory,
|
||||
categoryTitleHeight,
|
||||
itemMargin, smallItemWidth,
|
||||
subcategoryMargin
|
||||
} from "../../utils/landscapeCalculations";
|
||||
import LandscapeContext from '../../contexts/LandscapeContext'
|
||||
import SubcategoryInfo from '../SubcategoryInfo'
|
||||
import CategoryHeader from '../CategoryHeader'
|
||||
|
||||
const VerticalCategory = ({header, subcategories, top, left, width, height, color, href, fitWidth}) => {
|
||||
const subcategoriesWithCalculations = calculateVerticalCategory({ subcategories, fitWidth, width })
|
||||
const { guideIndex } = useContext(LandscapeContext)
|
||||
|
||||
return <div>
|
||||
<div style={{
|
||||
position: 'absolute', top, left, height, width, background: color,
|
||||
boxShadow: '0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.2)',
|
||||
padding: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
}} className="big-picture-section">
|
||||
<div style={{ height: categoryTitleHeight, width: '100%', display: 'flex' }}>
|
||||
<CategoryHeader href={href} label={header} guideAnchor={guideIndex[header]} background={color} />
|
||||
</div>
|
||||
<div style={{ width: '100%', position: 'relative', flex: 1, padding: `${subcategoryMargin}px 0`, display: 'flex', flexDirection: 'column', justifyContent: 'space-between', background: 'white' }}>
|
||||
{subcategoriesWithCalculations.map(subcategory => {
|
||||
const { width, columns, name } = subcategory
|
||||
const style = { display: 'grid', gridTemplateColumns: `repeat(${columns}, ${smallItemWidth}px)` }
|
||||
const extraStyle = fitWidth ? { justifyContent: 'space-evenly', flex: 1 } : { gridGap: itemMargin }
|
||||
const path = [header, name].join(' / ')
|
||||
|
||||
return <div key={subcategory.name} style={{position: 'relative', flexGrow: subcategory.rows, display: 'flex', flexDirection: 'column' }}>
|
||||
<div style={{ lineHeight: '15px', textAlign: 'center'}}>
|
||||
<InternalLink to={subcategory.href}>{name}</InternalLink>
|
||||
</div>
|
||||
|
||||
<div style={{width, overflow: 'hidden', margin: '0 auto', ...style, ...extraStyle}}>
|
||||
{subcategory.allItems.map(item => <Item item={item} key={item.name} fitWidth={fitWidth} />)}
|
||||
|
||||
{ guideIndex[path] && <SubcategoryInfo label={name} anchor={guideIndex[path]} column={columns}/> }
|
||||
</div>
|
||||
</div>
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
export default VerticalCategory
|
|
@ -1,11 +0,0 @@
|
|||
// locate zoom buttons
|
||||
|
||||
import React from 'react';
|
||||
import { pure } from 'recompose';
|
||||
|
||||
const Zoom = function({zoom, children}) {
|
||||
return <div style={{position:'relative', zoom: zoom}}>
|
||||
{children}
|
||||
</div>
|
||||
}
|
||||
export default pure(Zoom);
|
|
@ -1,39 +0,0 @@
|
|||
// locate zoom buttons
|
||||
import { pure } from 'recompose';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import RemoveCircleIcon from '@material-ui/icons/RemoveCircle';
|
||||
import AddCircleIcon from '@material-ui/icons/AddCircle';
|
||||
|
||||
import React, { useContext } from 'react';
|
||||
import { zoomLevels } from '../../utils/zoom'
|
||||
import LandscapeContext from '../../contexts/LandscapeContext'
|
||||
|
||||
const ZoomButtons = _ => {
|
||||
const { navigate, params } = useContext(LandscapeContext)
|
||||
const { zoom } = params
|
||||
|
||||
const minZoom = zoomLevels[0]
|
||||
const maxZoom = zoomLevels.slice(-1)[0]
|
||||
const zoomIndex = zoomLevels.indexOf(zoom)
|
||||
|
||||
const canZoomOut = zoom !== minZoom
|
||||
const canZoomIn = zoom !== maxZoom
|
||||
const zoomText = Math.round(zoom * 100) + '%'
|
||||
const setZoom = zoom => navigate({ zoom: zoom })
|
||||
const zoomIn = _ => setZoom(zoomLevels[zoomIndex + 1] || maxZoom)
|
||||
const zoomOut = _ => setZoom(zoomLevels[zoomIndex - 1] || minZoom)
|
||||
const resetZoom = _ => setZoom()
|
||||
|
||||
return <div className="zoom-buttons">
|
||||
<IconButton disabled={!canZoomOut} onClick={zoomOut} className='zoom-change' title="Zoom out" size="small">
|
||||
<RemoveCircleIcon />
|
||||
</IconButton>
|
||||
<Button onClick={resetZoom} className='zoom-reset' title="Reset zoom" size="small">{zoomText}</Button>
|
||||
<IconButton disabled={!canZoomIn} onClick={zoomIn} className='zoom-change' title="Zoom in" size="small">
|
||||
<AddCircleIcon />
|
||||
</IconButton>
|
||||
</div>
|
||||
}
|
||||
|
||||
export default pure(ZoomButtons);
|
|
@ -0,0 +1,64 @@
|
|||
const _ = require('lodash');
|
||||
const { assetPath } = require('../utils/assetPath');
|
||||
const { millify, h } = require('../utils/format');
|
||||
const { fields } = require('../types/fields');
|
||||
|
||||
function getRelationStyle(relation) {
|
||||
const relationInfo = fields.relation.valuesMap[relation]
|
||||
if (relationInfo && relationInfo.color) {
|
||||
return `border: 4px solid ${relationInfo.color};`;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.renderDefaultCard = function renderDefaultCard({item}) {
|
||||
return `
|
||||
<div data-id="${h(item.id)}" class="mosaic-wrap">
|
||||
<div class="mosaic ${item.oss ? '' : 'nonoss' }" style="${getRelationStyle(item.relation)}">
|
||||
<div class="logo_wrapper">
|
||||
<img loading="lazy" src="${assetPath(item.href)}" class="logo" max-height="100%" max-width="100%" alt="${h(item.name)}" />
|
||||
</div>
|
||||
<div class="mosaic-info">
|
||||
<div class="mosaic-title">
|
||||
<h5>${h(item.name)}</h5>
|
||||
${h(item.organization)}
|
||||
</div>
|
||||
<div class="mosaic-stars">
|
||||
${_.isNumber(item.stars) && item.stars ?
|
||||
`<div>
|
||||
<span>★</span>
|
||||
<span>${h(item.starsAsText)}</span>
|
||||
</div>` : ''
|
||||
}
|
||||
${Number.isInteger(item.amount) ?
|
||||
`<div class="mosaic-funding">${item.amountKind === 'funding' ? 'Funding: ': 'MCap: '} ${'$'+ h(millify(item.amount))}</div>` : ''
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
module.exports.renderFlatCard = function renderFlatCard({item}) {
|
||||
return `
|
||||
<div data-id="${item.id}" class="mosaic-wrap">
|
||||
<div class="mosaic">
|
||||
<img loading="lazy" src="${assetPath(item.href)}" class="logo" alt="${h(item.name)}" />
|
||||
<div class="separator"></div>
|
||||
<h5>${h(item.flatName)}</h5>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
module.exports.renderBorderlessCard = function renderBorderlessCard({item}) {
|
||||
return `
|
||||
<div data-id="${item.id}" class="mosaic-wrap">
|
||||
<div class="mosaic">
|
||||
<img loading="lazy" src="${assetPath(item.href)}" class="logo" alt="${h(item.name)}" />
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
import React, { useContext } from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
import Select from '@material-ui/core/Select';
|
||||
import TreeSelector from './TreeSelector';
|
||||
import LandscapeContext from '../contexts/LandscapeContext'
|
||||
import { options } from '../types/fields'
|
||||
|
||||
const CategoryFilter = _ => {
|
||||
const { navigate, params } = useContext(LandscapeContext)
|
||||
const { mainContentMode, filters } = params
|
||||
const isBigPicture = mainContentMode !== 'card-mode'
|
||||
const value = filters.landscape
|
||||
const _options = options('landscape')
|
||||
const onChange = landscape => navigate({ filters: { landscape }})
|
||||
|
||||
if (!isBigPicture) {
|
||||
return <TreeSelector value={value} options={_options} onChange={onChange} />;
|
||||
} else {
|
||||
return (
|
||||
<Select
|
||||
disabled
|
||||
value="empty"
|
||||
style={{width:175 ,fontSize:'0.8em'}}
|
||||
>
|
||||
<MenuItem value="empty">
|
||||
<em>N/A</em>
|
||||
</MenuItem>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
};
|
||||
export default pure(CategoryFilter);
|
|
@ -1,64 +1,40 @@
|
|||
import React from 'react'
|
||||
import css from 'styled-jsx/css'
|
||||
import GuideLink from './GuideLink'
|
||||
import { categoryTitleHeight } from '../utils/landscapeCalculations'
|
||||
import InternalLink from './InternalLink'
|
||||
import { getContrastRatio } from '@material-ui/core/styles'
|
||||
const getContrastRatio = require('get-contrast-ratio').default;
|
||||
|
||||
const CategoryHeader = ({ href, label, guideAnchor, background, rotate = false }) => {
|
||||
const { h } = require('../utils/format');
|
||||
const { renderGuideLink } = require('./GuideLink');
|
||||
const { categoryTitleHeight } = require('../utils/landscapeCalculations');
|
||||
|
||||
module.exports.renderCategoryHeader = function renderCategoryHeader({ href, label, guideAnchor, background, rotate = false }) {
|
||||
const lowContrast = getContrastRatio('#ffffff', background) < 4.5
|
||||
const color = lowContrast ? '#282828' : '#ffffff'
|
||||
const backgroundColor = lowContrast ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'
|
||||
const infoEl = css.resolve`
|
||||
a {
|
||||
width: ${categoryTitleHeight - 4}px;
|
||||
height: ${categoryTitleHeight - 4}px;
|
||||
margin: 2px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 18px;
|
||||
color: ${color};
|
||||
background: ${backgroundColor};
|
||||
transform: ${rotate ? 'rotate(180deg)' : 'none'};
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: ${color};
|
||||
background: none;
|
||||
}
|
||||
|
||||
a :global(svg) {
|
||||
stroke: ${color};
|
||||
}
|
||||
`
|
||||
|
||||
const linkEl = css.resolve`
|
||||
a {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
color: ${color};
|
||||
background: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
font-weight: bold;
|
||||
background: ${backgroundColor};
|
||||
}
|
||||
`
|
||||
|
||||
return <>
|
||||
{infoEl.styles}
|
||||
{linkEl.styles}
|
||||
|
||||
<InternalLink to={href} className={linkEl.className}>{label}</InternalLink>
|
||||
|
||||
{ guideAnchor && <GuideLink label={label} anchor={guideAnchor} className={infoEl.className} /> }
|
||||
</>
|
||||
return `
|
||||
<a data-type="internal"
|
||||
href="${href}"
|
||||
style="
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
color: ${color};
|
||||
background: none
|
||||
"
|
||||
>${h(label)}</a>
|
||||
${ guideAnchor ? renderGuideLink({label, anchor: guideAnchor, style: `
|
||||
width: ${categoryTitleHeight - 4}px;
|
||||
height: ${categoryTitleHeight - 4}px;
|
||||
margin: 2px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 18px;
|
||||
color: ${color};
|
||||
background: ${backgroundColor};
|
||||
transform: ${rotate ? 'rotate(180deg)' : 'none' };
|
||||
`}) : '' }
|
||||
`;
|
||||
}
|
||||
|
||||
export default CategoryHeader
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
import React from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import FormGroup from '@material-ui/core/FormGroup';
|
||||
import FormControlLabel from '@material-ui/core/FormControlLabel';
|
||||
import Checkbox from '@material-ui/core/Checkbox';
|
||||
|
||||
const CheckboxSelector = ({value, options, onChange}) => {
|
||||
const valueOf = function(checkbox) {
|
||||
return value.indexOf(checkbox) !== -1;
|
||||
};
|
||||
const handleCheckboxChange = function(checkbox, checked) {
|
||||
if (checked) {
|
||||
onChange(value.concat([checkbox]));
|
||||
} else {
|
||||
onChange(value.filter(function(x) { return x !== checkbox; }))
|
||||
}
|
||||
};
|
||||
|
||||
return <FormGroup>
|
||||
{ options.map( (el) => (
|
||||
<FormControlLabel key={el.id} control={
|
||||
<Checkbox onClick={function() {
|
||||
handleCheckboxChange(el.id, !valueOf(el.id));
|
||||
}}
|
||||
checked={valueOf(el.id)}
|
||||
/>
|
||||
} label={el.label}
|
||||
/>
|
||||
)) }
|
||||
</FormGroup>
|
||||
};
|
||||
export default pure(CheckboxSelector);
|
|
@ -1,40 +0,0 @@
|
|||
import React from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
import Select from '@material-ui/core/Select';
|
||||
import Checkbox from '@material-ui/core/Checkbox';
|
||||
import ListItemText from '@material-ui/core/ListItemText';
|
||||
|
||||
|
||||
const idToValue = (id) => id !== null ? id : 'any';
|
||||
const valueToId = (value) => value === 'any' ? null : value;
|
||||
|
||||
const ComboboxSelector = ({value, options, onChange}) => {
|
||||
const renderValue = function(selected) {
|
||||
if (selected.length === 0) {
|
||||
return 'Any';
|
||||
}
|
||||
return selected.join(', ');
|
||||
}
|
||||
|
||||
return <Select
|
||||
multiple
|
||||
style={{width:175, fontSize:'0.8em'}}
|
||||
value={idToValue(value)}
|
||||
onChange={(e) => onChange(valueToId(e.target.value))}
|
||||
renderValue={renderValue }
|
||||
displayEmpty
|
||||
>
|
||||
{ options.map( (el) => (
|
||||
<MenuItem key={idToValue(el.id)}
|
||||
value={idToValue(el.id)}
|
||||
style={{height:5}}
|
||||
>
|
||||
<Checkbox color="primary" disableRipple checked={value.indexOf(el.id) !== -1} />
|
||||
|
||||
<ListItemText disableTypography style={{fontSize:'0.8em'}} primary={el.label}/>
|
||||
</MenuItem>
|
||||
)) }
|
||||
</Select>
|
||||
};
|
||||
export default pure(ComboboxSelector);
|
|
@ -1,24 +0,0 @@
|
|||
import React from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
import Select from '@material-ui/core/Select';
|
||||
|
||||
|
||||
const idToValue = (id) => id !== null ? id : 'any';
|
||||
const valueToId = (value) => value === 'any' ? null : value;
|
||||
|
||||
const ComboboxSelector = ({value, options, onChange}) => {
|
||||
|
||||
return <Select
|
||||
style={{width:175, fontSize:'0.8em'}}
|
||||
value={idToValue(value)}
|
||||
onChange={(e) => onChange(valueToId(e.target.value))}
|
||||
>
|
||||
{ options.map( (el) => (
|
||||
<MenuItem key={idToValue(el.id)}
|
||||
value={idToValue(el.id)}
|
||||
style={{height:5, fontSize:'0.8em'}}>{el.label}</MenuItem>
|
||||
)) }
|
||||
</Select>
|
||||
};
|
||||
export default pure(ComboboxSelector);
|
|
@ -0,0 +1,64 @@
|
|||
const _ = require('lodash');
|
||||
// Render only for an export
|
||||
const { saneName } = require('../utils/saneName');
|
||||
const { h } = require('../utils/format');
|
||||
const { getGroupedItems, expandSecondPathItems } = require('../utils/itemsCalculator');
|
||||
const { parseParams } = require('../utils/routing');
|
||||
const { renderDefaultCard, renderBorderlessCard, renderFlatCard } = require('./CardRenderer');
|
||||
const icons = require('../utils/icons');
|
||||
|
||||
module.exports.render = function({items, exportUrl}) {
|
||||
const params = parseParams(exportUrl.split('?').slice(-1)[0]);
|
||||
if (params.grouping === 'landscape') {
|
||||
items = expandSecondPathItems(items);
|
||||
}
|
||||
const groupedItems = getGroupedItems({data: items, ...params})
|
||||
const cardStyle = params.cardStyle || 'default';
|
||||
const cardFn = cardStyle === 'borderless' ? renderBorderlessCard : cardStyle === 'flat' ? renderFlatCard : renderDefaultCard;
|
||||
const linkUrl = exportUrl.replace('&embed=yes', '').replace('embed=yes', '')
|
||||
|
||||
const result = `
|
||||
<div class="modal" style="display: none;">
|
||||
<div class="modal-shadow"></div>
|
||||
<div class="modal-container">
|
||||
<div class="modal-body">
|
||||
<div class="modal-buttons">
|
||||
<a class="modal-close">x</a>
|
||||
<span class="modal-prev">${icons.prev}</span>
|
||||
<span class="modal-next">${icons.next}</span>
|
||||
</div>
|
||||
<div class="modal-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="home" class="app ${cardStyle}-mode">
|
||||
<div class="app-overlay"></div>
|
||||
<div class="main-parent">
|
||||
<div class="app-overlay"></div>
|
||||
<div class="main">
|
||||
<div class="cards-section">
|
||||
<div class="column-content" >
|
||||
${ groupedItems.map( (groupedItem) => {
|
||||
const uniqItems = _.uniqBy(groupedItem.items, (x) => x.name + x.logo);
|
||||
const cardElements = uniqItems.map( (item) => cardFn({item}));
|
||||
const header = items.length > 0 ? `
|
||||
<div class="sh_wrapper" data-wrapper-id="${h(saneName(groupedItem.header))}">
|
||||
<div style="font-size: 24px; padding-left: 16px; line-height: 48px; font-weight: 500;">
|
||||
<span>${h(groupedItem.header)}</span>
|
||||
<span class="items-cont"> (${uniqItems.length})</span>
|
||||
</div>
|
||||
</div>` : '';
|
||||
return [ header, `<div data-section-id="${h(saneName(groupedItem.header))}">${cardElements.join('')}</div>`].join('');
|
||||
}) }
|
||||
</div>
|
||||
</div>
|
||||
<div id="embedded-footer">
|
||||
<h1 style="margin-top: 20px; width: 100%; text-align: center;">
|
||||
<a data-type="external" target="_blank" href="${linkUrl}">View</a> the full interactive landscape
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
return result;
|
||||
};
|
|
@ -1,14 +0,0 @@
|
|||
import React, { useContext } from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import OutboundLink from './OutboundLink';
|
||||
import LandscapeContext from '../contexts/LandscapeContext'
|
||||
import { stringifyParams } from '../utils/routing'
|
||||
|
||||
const EmbeddedFooter = () => {
|
||||
const { params } = useContext(LandscapeContext)
|
||||
const url = stringifyParams({ ...params, isEmbed: null })
|
||||
return <h1 style={{ marginTop: 20, width: '100%', textAlign: 'center' }}>
|
||||
<OutboundLink to={url}>View</OutboundLink> the full interactive landscape
|
||||
</h1>
|
||||
}
|
||||
export default pure(EmbeddedFooter);
|
|
@ -1,55 +0,0 @@
|
|||
import SystemUpdateIcon from '@material-ui/icons/SystemUpdate'
|
||||
import useSWR from 'swr'
|
||||
import assetPath from '../utils/assetPath'
|
||||
import { useContext, useEffect, useState } from 'react'
|
||||
import LandscapeContext from '../contexts/LandscapeContext'
|
||||
import Parser from 'json2csv/lib/JSON2CSVParser'
|
||||
import { flattenItems } from '../utils/itemsCalculator'
|
||||
|
||||
const fetchItems = shouldFetch => useSWR(shouldFetch ? assetPath(`/data/items-export.json`) : null)
|
||||
|
||||
const _downloadCSV = (allItems, selectedItems) => {
|
||||
const fields = allItems[0].map(([label, _]) => label !== 'id' && label).filter(_ => _)
|
||||
const itemsForExport = allItems
|
||||
.map(item => item.reduce((acc, [label, value]) => ({ ...acc, [label]: value }), {}))
|
||||
.filter(item => selectedItems[item.id])
|
||||
|
||||
const json2csvParser = new Parser({ fields });
|
||||
const csv = json2csvParser.parse(itemsForExport, { fields });
|
||||
const filename = 'interactive_landscape.csv'
|
||||
const data = 'data:text/csv;charset=utf-8,' + encodeURIComponent(csv)
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.setAttribute('href', data);
|
||||
link.setAttribute('download', filename);
|
||||
link.click();
|
||||
}
|
||||
|
||||
const ExportCsv = _ => {
|
||||
const { groupedItems } = useContext(LandscapeContext)
|
||||
const [shouldFetch, setShouldFetch] = useState(false)
|
||||
const { data: itemsForExport } = fetchItems(!!shouldFetch)
|
||||
const fetched = !!itemsForExport
|
||||
const selectedItems = flattenItems(groupedItems)
|
||||
.reduce((acc, item) => ({ ...acc, [item.id]: true }), {})
|
||||
const downloadCSV = () => _downloadCSV(itemsForExport, selectedItems)
|
||||
|
||||
const onClick = _ => {
|
||||
if (!fetched) {
|
||||
setShouldFetch(true)
|
||||
} else {
|
||||
downloadCSV()
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (fetched) {
|
||||
downloadCSV()
|
||||
}
|
||||
}, [fetched])
|
||||
|
||||
return <a className="filters-action" onClick={onClick} aria-label="Download as CSV">
|
||||
<SystemUpdateIcon/><span>Download as CSV</span>
|
||||
</a>
|
||||
};
|
||||
export default ExportCsv
|
|
@ -1,66 +0,0 @@
|
|||
import React from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import FormGroup from '@material-ui/core/FormGroup';
|
||||
import FormControl from '@material-ui/core/FormControl';
|
||||
import FormLabel from '@material-ui/core/FormLabel';
|
||||
|
||||
import ProjectFilterContainer from './ProjectFilterContainer';
|
||||
import LicenseFilterContainer from './LicenseFilterContainer';
|
||||
import OrganizationFilterContainer from './OrganizationFilterContainer';
|
||||
import HeadquartersFilterContainer from './HeadquartersFilterContainer';
|
||||
import ArrayFilterContainer from './ArrayFilterContainer';
|
||||
import fields from '../types/fields';
|
||||
import CategoryFilter from './CategoryFilter'
|
||||
const Filters = () => {
|
||||
return <div>
|
||||
<FormGroup row>
|
||||
<FormControl component="fieldset">
|
||||
<FormLabel component="legend">{fields.landscape.label}</FormLabel>
|
||||
<CategoryFilter/>
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup row>
|
||||
<FormControl component="fieldset">
|
||||
<FormLabel component="legend">{fields.relation.label}</FormLabel>
|
||||
<ProjectFilterContainer/>
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup row>
|
||||
<FormControl component="fieldset">
|
||||
<FormLabel component="legend">{fields.license.label}</FormLabel>
|
||||
<LicenseFilterContainer />
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup row>
|
||||
<FormControl component="fieldset">
|
||||
<FormLabel component="legend">{fields.organization.label}</FormLabel>
|
||||
<OrganizationFilterContainer />
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup row>
|
||||
<FormControl component="fieldset">
|
||||
<FormLabel component="legend">{fields.headquarters.label}</FormLabel>
|
||||
<HeadquartersFilterContainer />
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup row>
|
||||
<FormControl component="fieldset">
|
||||
<FormLabel component="legend">{fields.companyType.label}</FormLabel>
|
||||
<ArrayFilterContainer name="companyType" />
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup row>
|
||||
<FormControl component="fieldset">
|
||||
<FormLabel component="legend">{fields.industries.label}</FormLabel>
|
||||
<ArrayFilterContainer name="industries" />
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
</div>;
|
||||
}
|
||||
export default pure(Filters);
|
|
@ -1,14 +0,0 @@
|
|||
import React from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import OutboundLink from './OutboundLink';
|
||||
import settings from 'public/settings.json'
|
||||
|
||||
const Footer = () => {
|
||||
return <div style={{ marginTop: 10, fontSize:'9pt', width: '100%', textAlign: 'center' }}>
|
||||
{settings.home.footer} For more information, please see the
|
||||
<OutboundLink eventLabel="crunchbase-terms" to={`https://github.com/${settings.global.repo}/blob/HEAD/README.md#license`}>
|
||||
license
|
||||
</OutboundLink> info.
|
||||
</div>
|
||||
}
|
||||
export default pure(Footer);
|
|
@ -0,0 +1,61 @@
|
|||
const { calculateSize } = require("../utils/landscapeCalculations");
|
||||
const headerHeight = 40;
|
||||
module.exports.render = function({landscapeSettings, landscapeContent, version}) {
|
||||
const { fullscreenWidth, fullscreenHeight } = calculateSize(landscapeSettings);
|
||||
return `
|
||||
<div class="gradient-bg" style="
|
||||
width: ${fullscreenWidth}px;
|
||||
height: ${fullscreenHeight}px;
|
||||
overflow: hidden;
|
||||
"><div class="inner-landscape" style="
|
||||
width: ${fullscreenWidth}px;
|
||||
height: ${fullscreenHeight}px;
|
||||
padding-top: ${headerHeight + 20}px;
|
||||
padding-left: 20px;
|
||||
position: relative;
|
||||
">
|
||||
${landscapeContent }
|
||||
<div style="
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 18px,
|
||||
background: rgb(64,89,163);
|
||||
color: white;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
padding-top: 3px;
|
||||
padding-bottom: 3px;
|
||||
border-radius: 5px;
|
||||
">${landscapeSettings.fullscreen_header}</div>
|
||||
${ !landscapeSettings.fullscreen_hide_grey_logos ? `<div style="
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 12px;
|
||||
font-size: 11px;
|
||||
background: #eee;
|
||||
color: rgb(100,100,100);
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
padding-top: 3px;
|
||||
padding-bottom: 3px;
|
||||
border-radius: 5px;
|
||||
">Greyed logos are not open source</div>` : '' }
|
||||
<div style="
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 15px;
|
||||
font-size: 14px;
|
||||
color: white;
|
||||
">${landscapeSettings.title} </div>
|
||||
<div style="
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
left: 15px;
|
||||
font-size: 12px;
|
||||
color: #eee;
|
||||
">${version}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
import React from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import FormGroup from '@material-ui/core/FormGroup';
|
||||
import FormControl from '@material-ui/core/FormControl';
|
||||
import FormLabel from '@material-ui/core/FormLabel';
|
||||
import GroupingSelector from './GroupingSelector';
|
||||
const Grouping = () => {
|
||||
return <FormGroup row>
|
||||
<FormControl component="fieldset">
|
||||
<FormLabel component="legend">Grouping</FormLabel>
|
||||
<GroupingSelector />
|
||||
</FormControl>
|
||||
</FormGroup>;
|
||||
};
|
||||
export default pure(Grouping);
|
|
@ -1,38 +0,0 @@
|
|||
import React, { useContext } from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
import Select from '@material-ui/core/Select';
|
||||
import ComboboxSelector from './ComboboxSelector';
|
||||
import fields from '../types/fields'
|
||||
import LandscapeContext from '../contexts/LandscapeContext'
|
||||
|
||||
const groupingFields = ['landscape', 'relation', 'license', 'organization', 'headquarters'];
|
||||
const options = [{
|
||||
id: 'no',
|
||||
label: 'No Grouping',
|
||||
url: 'no'
|
||||
}].concat(groupingFields.map(id => ({ id, label: fields[id].groupingLabel })))
|
||||
|
||||
const GroupingSelector = _ => {
|
||||
const { navigate, params } = useContext(LandscapeContext)
|
||||
const { grouping, mainContentMode } = params
|
||||
const isBigPicture = mainContentMode !== 'card-mode'
|
||||
const onChange = grouping => navigate({ grouping })
|
||||
|
||||
if (!isBigPicture) {
|
||||
return <ComboboxSelector value={grouping} options={options} onChange={onChange} />;
|
||||
} else {
|
||||
return (
|
||||
<Select
|
||||
disabled
|
||||
value="empty"
|
||||
style={{width:175 ,fontSize:'0.8em'}}
|
||||
>
|
||||
<MenuItem value="empty">
|
||||
<em>N/A</em>
|
||||
</MenuItem>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
};
|
||||
export default pure(GroupingSelector);
|
|
@ -1,27 +1,10 @@
|
|||
import React from 'react'
|
||||
import css from 'styled-jsx/css'
|
||||
import InfoIcon from '@material-ui/icons/InfoOutlined'
|
||||
import OutboundLink from './OutboundLink'
|
||||
import assetPath from '../utils/assetPath'
|
||||
|
||||
const GuideLink = ({ anchor, label, className="" }) => {
|
||||
const { h } = require('../utils/format');
|
||||
const { guideLink } = require('../utils/icons');
|
||||
module.exports.renderGuideLink = function({anchor, label, style }) {
|
||||
const ariaLabel = `Read more about ${label} on the guide`
|
||||
const to = assetPath(`/guide#${anchor}`)
|
||||
const to = h(`guide#${anchor}`);
|
||||
|
||||
const svgEl = css.resolve`
|
||||
svg {
|
||||
stroke-width: 0;
|
||||
}
|
||||
|
||||
svg:hover {
|
||||
stroke-width: 0.5;
|
||||
}
|
||||
`
|
||||
|
||||
return <OutboundLink className={className} to={to} aria-label={ariaLabel}>
|
||||
{svgEl.styles}
|
||||
<InfoIcon style={{ fontSize: 'inherit' }} className={svgEl.className}/>
|
||||
</OutboundLink>
|
||||
return `<a data-type="external" target="_blank" style="${style}" aria-label="${h(ariaLabel)}" href="${to}">
|
||||
${guideLink}
|
||||
</a>`;
|
||||
}
|
||||
|
||||
export default GuideLink
|
||||
|
|
|
@ -0,0 +1,175 @@
|
|||
const _ = require('lodash');
|
||||
|
||||
const { sizeFn } = require('../utils/landscapeCalculations');
|
||||
const { renderItem } = require('./Item.js');
|
||||
const { h } = require('../utils/format');
|
||||
const { assetPath } = require('../utils/assetPath');
|
||||
const icons = require('../utils/icons');
|
||||
|
||||
|
||||
|
||||
// guide is a guide index
|
||||
module.exports.render = function({settings, items, guide}) {
|
||||
const currentBranch = require('child_process').execSync(`git rev-parse --abbrev-ref HEAD`, {
|
||||
cwd: require('../../tools/settings').projectPath
|
||||
}).toString().trim();
|
||||
|
||||
|
||||
const title = `<h1 className="title" style="margin-top: -5px;">${h(settings.global.short_name)} Landscape Guide</h1>`;
|
||||
const renderSubcategoryMetadata = ({ node, entries }) => {
|
||||
const orderedEntries = _.orderBy(entries, (x) => -x.size);
|
||||
const projectEntries = entries.filter(entry => entry.project)
|
||||
return `
|
||||
${ (node.buzzwords.length > 0 || projectEntries.length > 0) ? `<div class="metadata">
|
||||
<div class="header">
|
||||
<div>Buzzwords</div>
|
||||
<div>${h(settings.global.short_name)} Projects</div>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div>
|
||||
<ul>
|
||||
${ node.buzzwords.map(str => `<li>${h(str)}</li>`).join('') }
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<ul>
|
||||
${ projectEntries.map(entry => `<li>${h(entry.name)} (${h(entry.project)})</li>`).join('') }
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div> ` : '' }
|
||||
|
||||
<div class="items">
|
||||
${ orderedEntries.map(entry => renderItem(entry)).join('') }
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
const renderNavigation = ({ nodes }) => {
|
||||
const links = nodes.filter(node => node.anchor)
|
||||
const parents = links
|
||||
.map(n => n.anchor.split('--')[0])
|
||||
.reduce((acc, n) => ({ ...acc, [n]: (acc[n] || 0) + 1}), {})
|
||||
|
||||
return links
|
||||
.filter(({ title }) => {
|
||||
return title
|
||||
})
|
||||
.map(node => {
|
||||
const hasChildren = (parents[node.anchor] || 0) > 1
|
||||
return `
|
||||
<a href="#${node.anchor}" data-level="${node.level}" class="sidebar-link expandable" style="padding-left: ${10 + node.level * 10}px;">
|
||||
${h(node.title)} ${hasChildren ? icons.expand : ''}
|
||||
</a>
|
||||
${hasChildren ? `
|
||||
<a href="#${node.anchor}" data-level=${node.level + 1} class="sidebar-link" style="padding-left: 30px;"> Overview </a>
|
||||
` : ''}
|
||||
`}).join('');
|
||||
}
|
||||
|
||||
const renderLandscapeLink = ({ landscapeKey, title }) => {
|
||||
const href = `card-mode?category=${landscapeKey}`
|
||||
return `<a href="${href}" target="_blank" class="permalink">${icons.guide} ${h(title)} </a>`;
|
||||
}
|
||||
|
||||
const renderContent = ({ nodes, enhancedEntries }) => {
|
||||
return nodes.map((node) => {
|
||||
const subcategoryEntries = node.subcategory && enhancedEntries.filter(entry => entry.path.split(' / ')[1].trim() === node.title) || [];
|
||||
return `<div>
|
||||
${ node.title ? `<div class="section-title" id="${h(node.anchor)}">
|
||||
<h2 data-variant="${node.level + 1}">
|
||||
${ node.landscapeKey
|
||||
? renderLandscapeLink({landscapeKey: node.landscapeKey, title: node.title})
|
||||
: h(node.title)
|
||||
}
|
||||
</h2>
|
||||
</div>
|
||||
` : ''}
|
||||
${ node.content ? `<div class="guide-content">${node.content}</div>` : ''}
|
||||
${ node.subcategory ? renderSubcategoryMetadata({entries: subcategoryEntries,node:node}) : '' }
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
const enhancedEntries = items.map( (entry) => {
|
||||
let subcategory = entry.path.split(' / ')[1];
|
||||
let categoryAttrs = null;
|
||||
for (let key in settings.big_picture) {
|
||||
let page = settings.big_picture[key];
|
||||
for (let element of page.elements) {
|
||||
if (!page.category && element.category === entry.category) {
|
||||
categoryAttrs = element;
|
||||
}
|
||||
if (page.category === entry.category && element.category === subcategory) {
|
||||
categoryAttrs = element;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!categoryAttrs) {
|
||||
return null;
|
||||
}
|
||||
const enhanced = { ...entry, categoryAttrs }
|
||||
return { ...enhanced, size: sizeFn(enhanced) }
|
||||
}).filter( (x) => !!x);
|
||||
|
||||
return `
|
||||
<div class="links">
|
||||
<div>
|
||||
<a href="${(settings.global.self_hosted_repo || false) ? "" : "https://github.com/"}${settings.global.repo}/edit/${currentBranch}/guide.md" target="_blank">
|
||||
${icons.edit}
|
||||
Edit this page</a>
|
||||
</div>
|
||||
<div style="height: 5px;"></div>
|
||||
<div>
|
||||
<a href="${(settings.global.self_hosted_repo || false) ? "" : "https://github.com/"}${settings.global.repo}/issues/new?title=Guide Issue" target="_blank">
|
||||
${icons.github}
|
||||
Report issue</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="side-content">
|
||||
<span class="landscape-logo">
|
||||
<a aria-label="reset filters" class="nav-link" href="/">
|
||||
<img alt="landscape logo" src="${assetPath("images/left-logo.svg")} ">
|
||||
</a>
|
||||
</span>
|
||||
<div class="guide-sidebar">
|
||||
<div class="sidebar-collapse">+</div>
|
||||
<div class="guide-toggle">
|
||||
<span class="toggle-item "><a href="./">Landscape</a></span>
|
||||
<span class="toggle-item active">Guide</span>
|
||||
</div>
|
||||
${renderNavigation({nodes: guide})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="guide-header">
|
||||
<div class="container">
|
||||
<div class="content">
|
||||
<button class="sidebar-show" role="none" aria-label="show sidebar">${icons.sidebar}</button>
|
||||
<span class="landscape-logo">
|
||||
<a aria-label="reset filters" class="nav-link" href="/">
|
||||
<img alt="landscape logo" src="${assetPath("/images/left-logo.svg")}">
|
||||
</a>
|
||||
</span>
|
||||
${title}
|
||||
</div>
|
||||
</div>
|
||||
<a rel="noopener noreferrer noopener noreferrer"
|
||||
class="landscapeapp-logo"
|
||||
title="${h(settings.global.short_name)}"
|
||||
target="_blank"
|
||||
href="${settings.global.company_url}">
|
||||
<img src="${assetPath("images/right-logo.svg")}" title="${settings.global.short_name}">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="main-content">
|
||||
<div class="container">
|
||||
<div class="content">
|
||||
${renderContent({nodes: guide,enhancedEntries: enhancedEntries})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
import React, { useContext } from 'react'
|
||||
import Link from 'next/link'
|
||||
import LandscapeContext from '../contexts/LandscapeContext'
|
||||
|
||||
const ToggleItem = ({ isActive, title, to }) => {
|
||||
return <span className={`toggle-item ${isActive ? 'active' : ''}`}>
|
||||
<style jsx>{`
|
||||
.toggle-item {
|
||||
width: 50%;
|
||||
text-align: center;
|
||||
padding: 2px;
|
||||
color: #2E67BF;
|
||||
background: white;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.toggle-item.active {
|
||||
background: #2E67BF;
|
||||
color: white;
|
||||
}
|
||||
|
||||
a {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #2E67BF;
|
||||
}
|
||||
`}</style>
|
||||
{isActive ? title : <Link href={to} prefetch={false}>
|
||||
<a>{title}</a>
|
||||
</Link>}
|
||||
</span>
|
||||
}
|
||||
|
||||
const GuideToggle = ({ active }) => {
|
||||
const { guideIndex } = useContext(LandscapeContext)
|
||||
|
||||
if (active === 'landscape' && Object.keys(guideIndex).length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <div className="guide-toggle">
|
||||
<style jsx>{`
|
||||
.guide-toggle {
|
||||
border: 2px solid #2E67BF;
|
||||
background: #2E67BF;
|
||||
border-radius: 4px;
|
||||
max-width: 400px;
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
margin: 15px 0;
|
||||
max-width: 165px;
|
||||
}
|
||||
`}</style>
|
||||
<ToggleItem isActive={active === 'landscape'} title="Landscape" to="/" />
|
||||
<ToggleItem isActive={active === 'guide'} title="Guide" to="/guide" />
|
||||
</div>
|
||||
}
|
||||
|
||||
export default GuideToggle
|
|
@ -1,17 +0,0 @@
|
|||
import React from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import LandscapeLogo from './LandscapeLogo'
|
||||
import OrganizationLogo from '../OrganizationLogo'
|
||||
|
||||
const Header = _ => {
|
||||
return (
|
||||
<div className="header_container">
|
||||
<div className="header">
|
||||
<LandscapeLogo />
|
||||
<OrganizationLogo />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default pure(Header);
|
|
@ -1,14 +0,0 @@
|
|||
import TreeSelector from './TreeSelector';
|
||||
import { options } from '../types/fields';
|
||||
import { useContext } from 'react'
|
||||
import LandscapeContext from '../contexts/LandscapeContext'
|
||||
|
||||
const HeadquartersFilterContainer = () => {
|
||||
const { navigate, params } = useContext(LandscapeContext)
|
||||
const value = params.filters.headquarters
|
||||
const _options = options('headquarters')
|
||||
const onChange = value => navigate({ filters: { headquarters: value }})
|
||||
return <TreeSelector onChange={onChange} value={value} options={_options} />
|
||||
}
|
||||
|
||||
export default HeadquartersFilterContainer
|
|
@ -1,210 +0,0 @@
|
|||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import Head from 'next/head'
|
||||
import { pure } from 'recompose';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import CloseIcon from '@material-ui/icons/Close';
|
||||
import MenuIcon from '@material-ui/icons/Menu';
|
||||
import classNames from 'classnames'
|
||||
import Filters from './Filters';
|
||||
import Grouping from './Grouping';
|
||||
import Sorting from './Sorting';
|
||||
import Ad from './Ad';
|
||||
import OutboundLink from './OutboundLink';
|
||||
import TweetButton from './TweetButton';
|
||||
import Footer from './Footer';
|
||||
import EmbeddedFooter from './EmbeddedFooter';
|
||||
|
||||
import isGoogle from '../utils/isGoogle';
|
||||
import settings from 'public/settings.json'
|
||||
import useCurrentDevice from '../utils/useCurrentDevice'
|
||||
import LandscapeContent from './BigPicture/LandscapeContent'
|
||||
import LandscapeContext from '../contexts/LandscapeContext'
|
||||
import ResetFilters from './ResetFilters'
|
||||
import ItemDialog from './ItemDialog'
|
||||
import ZoomButtons from './BigPicture/ZoomButtons'
|
||||
import Summary from './Summary'
|
||||
import FullscreenButton from './BigPicture/FullscreenButton'
|
||||
import Header from './Header'
|
||||
import SwitchButton from './BigPicture/SwitchButton'
|
||||
import ExportCsv from './ExportCsv'
|
||||
import MainContent from './MainContent'
|
||||
import Presets from './Presets'
|
||||
import GuideToggle from './GuideToggle'
|
||||
|
||||
function preventDefault(e){
|
||||
const modal = e.srcElement.closest('.modal-body');
|
||||
if (!modal) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
function disableScroll(){
|
||||
const shadow = document.querySelector('.MuiBackdrop-root');
|
||||
if (shadow) {
|
||||
shadow.addEventListener('touchmove', preventDefault, { passive: false });
|
||||
}
|
||||
document.body.addEventListener('touchmove', preventDefault, { passive: false });
|
||||
}
|
||||
|
||||
function enableScroll(){
|
||||
const shadow = document.querySelector('.MuiBackdrop-root');
|
||||
if (shadow) {
|
||||
shadow.removeEventListener('touchmove', preventDefault);
|
||||
}
|
||||
document.body.removeEventListener('touchmove', preventDefault);
|
||||
}
|
||||
|
||||
const HomePage = _ => {
|
||||
const { params, landscapeSettings } = useContext(LandscapeContext)
|
||||
const { mainContentMode, zoom, isFullscreen, isEmbed, onlyModal, selectedItemId } = params
|
||||
const isBigPicture = mainContentMode !== 'card-mode';
|
||||
const [sidebarVisible, setSidebarVisible] = useState(false)
|
||||
const showSidebar = _ => setSidebarVisible(true)
|
||||
const hideSidebar = _ => setSidebarVisible(false)
|
||||
const [lastScrollPosition, setLastScrollPosition] = useState(0)
|
||||
const currentDevice = useCurrentDevice()
|
||||
|
||||
if (onlyModal) {
|
||||
document.querySelector('body').classList.add('popup');
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const { classList } = document.querySelector('html')
|
||||
isFullscreen ? classList.add('fullscreen') : classList.remove('fullscreen')
|
||||
}, [isFullscreen])
|
||||
|
||||
useEffect(() => {
|
||||
if (currentDevice.ios()) {
|
||||
if (selectedItemId) {
|
||||
if (!document.querySelector('.iphone-scroller')) {
|
||||
setLastScrollPosition((document.scrollingElement || document.body).scrollTop)
|
||||
}
|
||||
document.querySelector('html').classList.add('has-selected-item');
|
||||
(document.scrollingElement || document.body).scrollTop = 0;
|
||||
disableScroll();
|
||||
} else {
|
||||
document.querySelector('html').classList.remove('has-selected-item');
|
||||
if (document.querySelector('.iphone-scroller')) {
|
||||
(document.scrollingElement || document.body).scrollTop = lastScrollPosition;
|
||||
}
|
||||
enableScroll();
|
||||
}
|
||||
}
|
||||
}, [currentDevice, selectedItemId])
|
||||
|
||||
useEffect(() => {
|
||||
if (isEmbed) {
|
||||
if (window.parentIFrame) {
|
||||
if (selectedItemId) {
|
||||
window.parentIFrame.sendMessage({type: 'showModal'})
|
||||
} else {
|
||||
window.parentIFrame.sendMessage({type: 'hideModal'})
|
||||
}
|
||||
if (selectedItemId) {
|
||||
window.parentIFrame.getPageInfo(function(info) {
|
||||
var offset = info.scrollTop - info.offsetTop;
|
||||
var height = info.iframeHeight - info.clientHeight;
|
||||
var maxHeight = info.clientHeight * 0.9;
|
||||
if (maxHeight > 480) {
|
||||
maxHeight = 480;
|
||||
}
|
||||
var t = function(x1, y1, x2, y2, x3) {
|
||||
if (x3 < x1 - 50) {
|
||||
x3 = x1 - 50;
|
||||
}
|
||||
if (x3 > x2 + 50) {
|
||||
x3 = x2 + 50;
|
||||
}
|
||||
return y1 + (x3 - x1) / (x2 - x1) * (y2 - y1);
|
||||
}
|
||||
var top = t(0, -height, height, height, offset);
|
||||
if (top < 0 && info.iframeHeight <= 600) {
|
||||
top = 10;
|
||||
}
|
||||
setTimeout(function() {
|
||||
const modal = document.querySelector('.modal-body');
|
||||
if (modal) {
|
||||
modal.style.top = top + 'px';
|
||||
modal.style.maxHeight = maxHeight + 'px';
|
||||
}
|
||||
}, 10);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const { classList } = document.documentElement
|
||||
if (!classList.contains('embed')) {
|
||||
classList.add('embed');
|
||||
}
|
||||
}
|
||||
}, [selectedItemId])
|
||||
|
||||
if ((isGoogle() || onlyModal) && selectedItemId) {
|
||||
return <ItemDialog />;
|
||||
}
|
||||
|
||||
const isIphone = currentDevice.ios()
|
||||
const titlePrefix = isBigPicture ? (landscapeSettings.isMain ? '' : landscapeSettings.name) : 'Card Mode'
|
||||
const title = [titlePrefix, settings.global.meta.title].filter(_ => _).join(' - ')
|
||||
|
||||
return <>
|
||||
{selectedItemId && <ItemDialog/>}
|
||||
<Head>
|
||||
<title>{title}</title>
|
||||
<meta property="og:title" content={title}/>
|
||||
</Head>
|
||||
<div id="home" className={classNames('app',{'filters-opened' : sidebarVisible, 'big-picture': isBigPicture })}>
|
||||
<div style={{marginTop: isIphone && selectedItemId ? -lastScrollPosition : 0}} className={classNames({"iphone-scroller": isIphone && selectedItemId}, 'main-parent')} >
|
||||
{ !isEmbed && !isFullscreen && <>
|
||||
<Header />
|
||||
<IconButton className="sidebar-show" title="Show sidebar" onClick={showSidebar}><MenuIcon /></IconButton>
|
||||
<div className="sidebar">
|
||||
<div className="sidebar-scroll">
|
||||
<IconButton className="sidebar-collapse" title="Hide sidebar" size="small" onClick={hideSidebar}><CloseIcon /></IconButton>
|
||||
<GuideToggle active="landscape"/>
|
||||
<ResetFilters />
|
||||
<Grouping/>
|
||||
<Sorting/>
|
||||
<Filters />
|
||||
<Presets />
|
||||
<ExportCsv />
|
||||
<Ad />
|
||||
</div>
|
||||
</div>
|
||||
</>}
|
||||
|
||||
{sidebarVisible && <div className="app-overlay" onClick={hideSidebar}></div>}
|
||||
|
||||
<div className={classNames('main', {'embed': isEmbed})}>
|
||||
<div className="disclaimer">
|
||||
<span dangerouslySetInnerHTML={{__html: settings.home.header}} />
|
||||
Please <OutboundLink to={`https://github.com/${settings.global.repo}`}>open</OutboundLink> a pull request to
|
||||
correct any issues. Greyed logos are not open source. Last Updated: {process.env.lastUpdated}
|
||||
</div>
|
||||
<Summary />
|
||||
|
||||
<div className="cards-section">
|
||||
<SwitchButton />
|
||||
<div className="right-buttons">
|
||||
<TweetButton cls="tweet-button-main"/>
|
||||
<FullscreenButton/>
|
||||
<ZoomButtons/>
|
||||
</div>
|
||||
{ isBigPicture &&
|
||||
<div className="landscape-flex">
|
||||
<div className="landscape-wrapper">
|
||||
<LandscapeContent zoom={zoom} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{ !isBigPicture && <MainContent /> }
|
||||
</div>
|
||||
{ !isEmbed && !isBigPicture && <Footer/> }
|
||||
{ isEmbed && <EmbeddedFooter/> }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
};
|
||||
|
||||
export default pure(HomePage);
|
|
@ -0,0 +1,237 @@
|
|||
const _ = require('lodash');
|
||||
const { h } = require('../utils/format');
|
||||
const { fields, sortOptions, options } = require('../types/fields');
|
||||
const { assetPath } = require('../utils/assetPath');
|
||||
const icons = require('../utils/icons');
|
||||
|
||||
const renderSingleSelect = ({name, options, title}) => (
|
||||
`
|
||||
<div class="select" data-type="single" data-name="${name}" data-options="${h(JSON.stringify(options))}">
|
||||
<select class="select-text" required>
|
||||
<option value="1" selected>Value</option>
|
||||
</select>
|
||||
<span class="select-highlight"></span>
|
||||
<span class="select-bar"></span>
|
||||
<label class="select-label">${h(title)}</label>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
const renderMultiSelect = ({name, options, title}) => (
|
||||
`
|
||||
<div class="select" data-type="multi" data-name="${name}" data-options="${h(JSON.stringify(options))}">
|
||||
<select class="select-text" required>
|
||||
<option value="1" selected>Value</option>
|
||||
</select>
|
||||
<span class="select-highlight"></span>
|
||||
<span class="select-bar"></span>
|
||||
<label class="select-label">${h(title)}</label>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
|
||||
const renderGroupingSelect = function() {
|
||||
const groupingFields = ['landscape', 'relation', 'license', 'organization', 'headquarters'];
|
||||
const options = [{
|
||||
id: 'no',
|
||||
label: 'No Grouping',
|
||||
}].concat(groupingFields.map(id => ({ id: fields[id].url, label: (fields[id].groupingLabel) })))
|
||||
return renderSingleSelect({name: "grouping", options, title: "Grouping" });
|
||||
}
|
||||
|
||||
const renderSortBySelect = function() {
|
||||
const options = sortOptions.filter( (x) => !x.disabled).map( (x) => ({
|
||||
id: (fields[x.id] || { url: x.id}).url || x.id, label: x.label
|
||||
}))
|
||||
return renderSingleSelect({name: "sort", options, title: "Sort By" });
|
||||
}
|
||||
|
||||
const renderFilterCategory = function() {
|
||||
return renderMultiSelect({name:"category", options: options('landscape'), title: 'Category'});
|
||||
}
|
||||
|
||||
const renderFilterProject = function() {
|
||||
return renderMultiSelect({name:"project", options: options('relation'), title: 'Project'});
|
||||
}
|
||||
|
||||
const renderFilterLicense = function() {
|
||||
return renderMultiSelect({name:"license", options: options('license'), title: "License"});
|
||||
}
|
||||
|
||||
const renderFilterOrganization = function() {
|
||||
return renderMultiSelect({name: "organization", options: options('organization'), title: "Organization"});
|
||||
}
|
||||
|
||||
const renderFilterHeadquarters = function() {
|
||||
return renderMultiSelect({name: "headquarters", options: options('headquarters'), title: "Headquarters"});
|
||||
}
|
||||
|
||||
const renderFilterCompanyType = function() {
|
||||
return renderMultiSelect({name: "company-type", options: options('companyType'), title: "Company Type"});
|
||||
}
|
||||
|
||||
const renderFilterIndustries = function() {
|
||||
return renderMultiSelect({name: "industries", options: options('industries'), title: "Industry"});
|
||||
}
|
||||
|
||||
module.exports.render = function({settings, guidePayload, hasGuide, bigPictureKey}) {
|
||||
const mainCard = [{shortTitle: 'Card', title: 'Card Mode', mode: 'card', url: 'card-mode', tabIndex: 0}]
|
||||
const landscapes = Object.values(settings.big_picture).map(function(section) {
|
||||
return {
|
||||
url: section.url,
|
||||
title: section.name,
|
||||
shortTitle: section.short_name,
|
||||
mode: section.url === settings.big_picture.main.url ? 'main' : section.url,
|
||||
tabIndex: section.tab_index
|
||||
}
|
||||
})
|
||||
const tabs = _.orderBy(mainCard.concat(landscapes), 'tabIndex').map( item => _.pick(item, ['title', 'mode', 'shortTitle', 'url']))
|
||||
|
||||
|
||||
return `
|
||||
<div class="select-popup" style="display: none;">
|
||||
<div class="select-popup-body" ></div>
|
||||
</div>
|
||||
<div class="modal" style="display: none;">
|
||||
<div class="modal-shadow" ></div>
|
||||
<div class="modal-container">
|
||||
<div class="modal-body">
|
||||
<div class="modal-buttons">
|
||||
<a class="modal-close">x</a>
|
||||
<span class="modal-prev">${icons.prev}</span>
|
||||
<span class="modal-next">${icons.next}</span>
|
||||
</div>
|
||||
<div class="modal-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="guide-page" style="display: ${guidePayload ? "" : "none"};" data-loaded="${guidePayload ? "true" : ""}">
|
||||
${ !guidePayload ? `<div class="side-content">
|
||||
<span class="landscape-logo">
|
||||
<a aria-label="reset filters" class="nav-link" href="/">
|
||||
<img alt="landscape logo" src="${assetPath("images/left-logo.svg")}" />
|
||||
</a>
|
||||
</span>
|
||||
<div class="guide-sidebar">
|
||||
<div class="sidebar-collapse">X</div>
|
||||
<div class="guide-toggle">
|
||||
<span class="toggle-item "><a href="./">Landscape</a></span>
|
||||
<span class="toggle-item active">Guide</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>` : ''
|
||||
}
|
||||
${ guidePayload ? "$$guide$$" : ''}
|
||||
</div>
|
||||
<div id="home" style="display: ${guidePayload ? "none" : ""}" class="app">
|
||||
<div class="app-overlay"></div>
|
||||
<div class="main-parent">
|
||||
<button class="sidebar-show" role="none" aria-label="show sidebar">${icons.sidebar}</button>
|
||||
<div class="header_container">
|
||||
<div class="header">
|
||||
<span class="landscape-logo">
|
||||
<a aria-label="reset filters" class="nav-link" href="/">
|
||||
<img alt="landscape logo" src="${assetPath("images/left-logo.svg")}" />
|
||||
</a>
|
||||
</span>
|
||||
<a rel="noopener noreferrer noopener noreferrer"
|
||||
class="landscapeapp-logo"
|
||||
title="${h(settings.global.short_name)}"
|
||||
target="_blank"
|
||||
href="${settings.global.company_url}">
|
||||
<img src="${assetPath("/images/right-logo.svg")}" title="${h(settings.global.short_name)}" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sidebar">
|
||||
<div class="sidebar-scroll">
|
||||
<div class="sidebar-collapse">+</div>
|
||||
${ hasGuide ? `
|
||||
<div class="guide-toggle">
|
||||
<span class="toggle-item active">Landscape</span>
|
||||
<span class="toggle-item "><a href="/guide">Guide</a></span>
|
||||
</div> ` : ''
|
||||
}
|
||||
<a class="filters-action reset-filters">${icons.reset}<span>Reset Filters</span>
|
||||
</a>
|
||||
${renderGroupingSelect()}
|
||||
${renderSortBySelect()}
|
||||
${renderFilterCategory()}
|
||||
${renderFilterProject()}
|
||||
${renderFilterLicense()}
|
||||
${renderFilterOrganization()}
|
||||
${renderFilterHeadquarters()}
|
||||
${renderFilterCompanyType()}
|
||||
${renderFilterIndustries()}
|
||||
|
||||
<div class="sidebar-presets">
|
||||
<h4>Example filters</h4>
|
||||
${ (settings.presets || []).map(preset => `
|
||||
<a data-type="internal" class="preset" href="${preset.url}">
|
||||
${h(preset.label)}
|
||||
</a> `
|
||||
).join('')}
|
||||
</div>
|
||||
${ (settings.ads || []).map( (entry) => `
|
||||
<a data-type="external" target="_blank" class="sidebar-event" href="${entry.url}" title="${h(entry.title)}">
|
||||
<img src="${assetPath(entry.image)}" alt="${entry.title}" />
|
||||
</a>
|
||||
`).join('') }
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="app-overlay"></div>
|
||||
|
||||
<div class="main">
|
||||
<div class="disclaimer">
|
||||
<span> ${settings.home.header} </span>
|
||||
Please <a data-type="external" target="_blank" href="${(settings.global.self_hosted_repo || false) ? "" : "https://github.com/"}${settings.global.repo}">open</a> a pull request to
|
||||
correct any issues. Greyed logos are not open source. Last Updated: ${process.env.lastUpdated}
|
||||
</div>
|
||||
<h4 class="summary"></h4>
|
||||
<div class="cards-section">
|
||||
<div class="big-picture-switch big-picture-switch-normal">
|
||||
${ tabs.map( (tab) => `
|
||||
<a href="${tab.url}" data-mode="${tab.mode}"><div>${h(tab.title)}</div></a>
|
||||
`).join('')}
|
||||
</div>
|
||||
|
||||
<div class="right-buttons">
|
||||
<div class="fullscreen-exit">${icons.fullscreenExit}</div>
|
||||
<div class="fullscreen-enter">${icons.fullscreenEnter}</div>
|
||||
<div class="zoom-out">${icons.zoomOut}</div>
|
||||
<div class="zoom-reset"></div>
|
||||
<div class="zoom-in">${icons.zoomIn}</div>
|
||||
</div>
|
||||
|
||||
${ tabs.filter( (x) => x.mode !== 'card').map( (tab) => `
|
||||
<div data-mode="${tab.mode}" class="landscape-flex">
|
||||
<div class="landscape-wrapper">
|
||||
<div class="inner-landscape" style="padding: 10px; display: none;">
|
||||
${ bigPictureKey === tab.mode ? '$$' + bigPictureKey + '$$' : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
|
||||
<div class="column-content"></div>
|
||||
</div>
|
||||
<div id="footer" style="
|
||||
margin-top: 10px;
|
||||
font-size: 9pt;
|
||||
width: 100%;
|
||||
text-align: center;">
|
||||
${h(settings.home.footer)} For more information, please see the
|
||||
<a data-type="external" target="_blank" eventLabel="crunchbase-terms" href="${(settings.global.self_hosted_repo || false) ? "" : "https://github.com/"}${settings.global.repo}/blob/HEAD/README.md#license">
|
||||
license
|
||||
</a> info.
|
||||
</div>
|
||||
<div id="embedded-footer">
|
||||
<h1 style="margin-top: 20px; width: 100%; text-align: center;">
|
||||
<a data-type="external" target="_blank" href="url">View</a> the full interactive landscape
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
const { renderItem } = require("./Item");
|
||||
const { h } = require('../utils/format');
|
||||
const {
|
||||
calculateHorizontalCategory,
|
||||
categoryBorder,
|
||||
categoryTitleHeight,
|
||||
dividerWidth,
|
||||
itemMargin,
|
||||
smallItemWidth,
|
||||
smallItemHeight,
|
||||
subcategoryMargin,
|
||||
subcategoryTitleHeight
|
||||
} = require("../utils/landscapeCalculations");
|
||||
const { renderSubcategoryInfo } = require('./SubcategoryInfo');
|
||||
const { renderCategoryHeader } = require('./CategoryHeader');
|
||||
|
||||
const renderDivider = (color) => {
|
||||
const width = dividerWidth;
|
||||
const marginTop = 2 * subcategoryMargin;
|
||||
const height = `calc(100% - ${2 * marginTop}px)`;
|
||||
|
||||
return `<div style="
|
||||
width: ${width}px;
|
||||
margin-top: ${marginTop}px;
|
||||
height: ${height};
|
||||
border-left: ${width}px solid ${color}
|
||||
"></div>`;
|
||||
}
|
||||
|
||||
module.exports.renderHorizontalCategory = function({ header, guideInfo, subcategories, width, height, top, left, color, href, fitWidth }) {
|
||||
const addInfoIcon = !!guideInfo;
|
||||
const subcategoriesWithCalculations = calculateHorizontalCategory({ height, width, subcategories, fitWidth, addInfoIcon })
|
||||
const totalRows = Math.max(...subcategoriesWithCalculations.map(({ rows }) => rows))
|
||||
|
||||
return `
|
||||
<div style="
|
||||
width: ${width}px;
|
||||
left: ${left}px;
|
||||
height: ${height}px;
|
||||
top: ${top}px;
|
||||
position: absolute;
|
||||
" class="big-picture-section">
|
||||
<div
|
||||
style="
|
||||
position: absolute;
|
||||
background: ${color};
|
||||
top: ${subcategoryTitleHeight}px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.2);
|
||||
padding: ${categoryBorder}px;
|
||||
"
|
||||
>
|
||||
<div style="
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: ${categoryTitleHeight}px;
|
||||
position: absolute;
|
||||
writing-mode: vertical-rl;
|
||||
transform: rotate(180deg);
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
">
|
||||
${renderCategoryHeader({href, label: header, guideAnchor: guideInfo, background: color,rotate: true})}
|
||||
</div>
|
||||
<div style="
|
||||
margin-left: 30px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
background: white;
|
||||
">
|
||||
${subcategoriesWithCalculations.map((subcategory, index) => {
|
||||
const lastSubcategory = index !== subcategories.length - 1
|
||||
const { allItems, guideInfo, columns, width, name, href } = subcategory
|
||||
const padding = fitWidth ? 0 : `${subcategoryMargin}px 0`;
|
||||
const style = `
|
||||
display: grid;
|
||||
height: 100%;
|
||||
grid-template-columns: repeat(${columns}, ${smallItemWidth}px);
|
||||
grid-auto-rows: ${smallItemHeight}px;
|
||||
`;
|
||||
const extraStyle = fitWidth ? `justify-content: space-evenly; align-content: space-evenly;` : `grid-gap: ${itemMargin}px;`;
|
||||
return `
|
||||
<div style="
|
||||
width: ${width}px;
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
padding: ${padding};
|
||||
box-sizing: border-box;
|
||||
">
|
||||
<div style="
|
||||
position: absolute;
|
||||
top: ${-1 * categoryTitleHeight}px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: ${categoryTitleHeight}px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
">
|
||||
<a data-type="internal" href="${href}" class="white-link">${h(name)}</a>
|
||||
</div>
|
||||
<div style="${style} ${extraStyle}">
|
||||
${allItems.map(renderItem).join('')}
|
||||
${guideInfo ? renderSubcategoryInfo({label: name, anchor: guideInfo,column: columns, row:totalRows}) : ''}
|
||||
</div>
|
||||
</div>
|
||||
${lastSubcategory ? renderDivider(color) : ''}
|
||||
`
|
||||
}).join('')}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
import React, { useContext } from 'react';
|
||||
import Link from 'next/link'
|
||||
import isGoogle from '../utils/isGoogle';
|
||||
import LandscapeContext from '../contexts/LandscapeContext'
|
||||
|
||||
const InternalLink = ({to, children, onClick, className, ...other}) => {
|
||||
const { params } = useContext(LandscapeContext)
|
||||
if (params.isEmbed || isGoogle() || params.onlyModal || !to) {
|
||||
return <span className={`${className}`} {...other}>{children}</span>;
|
||||
} else {
|
||||
return <Link href={to} prefetch={false}>
|
||||
<a className={`${className} nav-link`} {...other}>{children}</a>
|
||||
</Link>
|
||||
}
|
||||
}
|
||||
export default InternalLink
|
||||
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
const { assetPath } = require('../utils/assetPath');
|
||||
const { fields } = require("../types/fields");
|
||||
const { h } = require('../utils/format');
|
||||
|
||||
const { readJsonFromDist } = require('../utils/readJson');
|
||||
const settings = readJsonFromDist('settings');
|
||||
|
||||
const largeItem = function(item) {
|
||||
const isMember = item.category === settings.global.membership;
|
||||
const relationInfo = fields.relation.valuesMap[item.relation]
|
||||
if (!relationInfo) {
|
||||
console.error(`no map for ${item.relation} on ${item.name}`);
|
||||
}
|
||||
const color = relationInfo.big_picture_color;
|
||||
const label = relationInfo.big_picture_label;
|
||||
const textHeight = label ? 10 : 0
|
||||
const padding = 2
|
||||
|
||||
const isMultiline = h(label).length > 20;
|
||||
const formattedLabel = isMultiline ? h(label).replace(' - ', '<br>') : h(label);
|
||||
|
||||
if (isMember) {
|
||||
return `
|
||||
<div data-id="${item.id}" class="large-item large-item-${item.size} item">
|
||||
<img loading="lazy" src="${assetPath(item.href)}" alt="${item.name}" style="
|
||||
width: calc(100% - ${2 * padding}px);
|
||||
height: calc(100% - ${2 * padding + textHeight}px);
|
||||
padding: 5px;
|
||||
margin: ${padding}px ${padding}px 0 ${padding}px;
|
||||
"/>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div data-id="${item.id}" class="large-item large-item-${item.size} item" style="background: ${color}">
|
||||
<img loading="lazy" src="${assetPath(item.href)}" alt="${item.name}" style="
|
||||
width: calc(100% - ${2 * padding}px);
|
||||
height: calc(100% - ${2 * padding + textHeight}px);
|
||||
padding: 5px;
|
||||
margin: ${padding}px ${padding}px 0 ${padding}px;
|
||||
"/>
|
||||
<div class="label" style="
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: ${textHeight + padding + (isMultiline ? 6 : 0) }px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
background: ${color};
|
||||
color: white;
|
||||
font-size: 6.7px;
|
||||
line-height: ${isMultiline ? 9 : 13 }px;
|
||||
">${ formattedLabel }</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const smallItem = function(item) {
|
||||
const isMember = item.category === settings.global.membership;
|
||||
return `
|
||||
<img data-id="${item.id}"
|
||||
loading="lazy"
|
||||
class="item small-item"
|
||||
src="${assetPath(item.href)}"
|
||||
alt="${h(item.name)}"
|
||||
style="border-color: ${isMember ? 'white' : ''};"
|
||||
/>`
|
||||
}
|
||||
|
||||
module.exports.renderItem = function (item) {
|
||||
const {size, category, oss, categoryAttrs } = item;
|
||||
const isMember = category === settings.global.membership;
|
||||
const ossClass = isMember || oss || (categoryAttrs.isLarge && !settings.global.flags?.gray_large_items) ? 'oss' : 'nonoss';
|
||||
const isLargeClass = size > 1 ? `wrapper-large-${size}` : '';
|
||||
|
||||
return `<div class="${isLargeClass + ' item-wrapper ' + ossClass}">
|
||||
${size > 1 ? largeItem({isMember, ...item}) : smallItem({...item})}
|
||||
</div>`;
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
import React, { useContext } from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
import classNames from 'classnames'
|
||||
import ItemDialogContent from './ItemDialogContent';
|
||||
import ItemDialogButtons from './ItemDialogButtons'
|
||||
import LandscapeContext from '../contexts/LandscapeContext'
|
||||
import useSWR from 'swr'
|
||||
import assetPath from '../utils/assetPath'
|
||||
|
||||
const fetchItem = itemId => useSWR(itemId ? assetPath(`/data/items/${itemId}.json`) : null)
|
||||
|
||||
const ItemDialog = _ => {
|
||||
const { navigate, params, entries } = useContext(LandscapeContext)
|
||||
const { onlyModal, selectedItemId } = params
|
||||
const { data: selectedItem } = fetchItem(selectedItemId)
|
||||
const closeDialog = _ => onlyModal ? _ : navigate({ selectedItemId: null }, { scroll: false })
|
||||
const nonoss = selectedItem && selectedItem.oss === false
|
||||
const loading = selectedItemId && !selectedItem
|
||||
const itemInfo = selectedItem || entries.find(({ id }) => id === selectedItemId)
|
||||
|
||||
return (
|
||||
<Dialog open={!!selectedItemId} onClose={closeDialog} transitionDuration={400}
|
||||
classes={{paper:'modal-body'}}
|
||||
className={classNames('modal', 'product', {nonoss})}>
|
||||
{ !onlyModal && <ItemDialogButtons closeDialog={closeDialog} /> }
|
||||
<ItemDialogContent itemInfo={itemInfo} loading={loading}/>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
export default pure(ItemDialog);
|
|
@ -1,26 +0,0 @@
|
|||
import React, { useContext } from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import ChevronLeftIcon from '@material-ui/icons/ChevronLeft';
|
||||
import ChevronRightIcon from '@material-ui/icons/ChevronRight';
|
||||
import KeyHandler from 'react-key-handler';
|
||||
import LandscapeContext from '../contexts/LandscapeContext'
|
||||
|
||||
|
||||
const ItemDialogButtons = ({ closeDialog }) => {
|
||||
const { navigate, nextItemId, previousItemId } = useContext(LandscapeContext)
|
||||
const onSelectItem = selectedItemId => navigate({ selectedItemId }, { scroll: false })
|
||||
return (
|
||||
<div className='modal-buttons'>
|
||||
{ nextItemId && <KeyHandler keyValue="ArrowRight" onKeyHandle={() => onSelectItem(nextItemId)} /> }
|
||||
{ previousItemId && <KeyHandler keyValue="ArrowLeft" onKeyHandle={() => onSelectItem(previousItemId)} /> }
|
||||
<a className="modal-close" onClick={closeDialog}>×</a>
|
||||
<span className="modal-prev" disabled={!previousItemId} onClick={(e) => {e.stopPropagation(); onSelectItem(previousItemId)}}>
|
||||
<ChevronLeftIcon style={{ fontSize:'1.2em'}} />
|
||||
</span>
|
||||
<span className="modal-next" disabled={!nextItemId} onClick={(e) => {e.stopPropagation(); onSelectItem(nextItemId)}}>
|
||||
<ChevronRightIcon style={{ fontSize:'1.2em'}} />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default pure(ItemDialogButtons);
|
|
@ -1,661 +0,0 @@
|
|||
import React, { Fragment, useContext, useEffect, useState } from 'react';
|
||||
import SvgIcon from '@material-ui/core/SvgIcon';
|
||||
import StarIcon from '@material-ui/icons/Star';
|
||||
import KeyHandler from 'react-key-handler';
|
||||
import _ from 'lodash';
|
||||
import OutboundLink from './OutboundLink';
|
||||
import millify from 'millify';
|
||||
import relativeDate from 'relative-date';
|
||||
import formatNumber from '../utils/formatNumber';
|
||||
import isParent from '../utils/isParent';
|
||||
import InternalLink from './InternalLink';
|
||||
import fields from '../types/fields';
|
||||
import isGoogle from '../utils/isGoogle';
|
||||
import settings from 'public/settings.json';
|
||||
import TweetButton from './TweetButton';
|
||||
import TwitterTimeline from "./TwitterTimeline";
|
||||
import {Bar, Pie, defaults} from 'react-chartjs-2';
|
||||
import 'chartjs-adapter-date-fns';
|
||||
import classNames from 'classnames'
|
||||
import CreateWidthMeasurer from 'measure-text-width';
|
||||
import assetPath from '../utils/assetPath'
|
||||
import { stringifyParams } from '../utils/routing'
|
||||
import LandscapeContext from '../contexts/LandscapeContext'
|
||||
import Head from 'next/head'
|
||||
import useWindowSize from '../utils/useWindowSize'
|
||||
|
||||
const closeUrl = params => stringifyParams({ mainContentMode: 'card-mode', selectedItemId: null, ...params })
|
||||
|
||||
let productScrollEl = null;
|
||||
const formatDate = function(x) {
|
||||
if (x.text) {
|
||||
return x.text;
|
||||
}
|
||||
return relativeDate(new Date(x));
|
||||
};
|
||||
const formatTwitter = function(x) {
|
||||
const name = x.split('/').slice(-1)[0];
|
||||
return '@' + name;
|
||||
}
|
||||
|
||||
function getRelationStyle(relation) {
|
||||
const relationInfo = fields.relation.valuesMap[relation]
|
||||
if (relationInfo && relationInfo.color) {
|
||||
return {
|
||||
border: '4px solid ' + relationInfo.color
|
||||
};
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const showTwitter = !isGoogle();
|
||||
|
||||
const iconGithub = <svg viewBox="0 0 24 24">
|
||||
<path d="M12,2A10,10 0 0,0 2,12C2,16.42 4.87,20.17 8.84,21.5C9.34,21.58
|
||||
9.5,21.27 9.5,21C9.5,20.77 9.5,20.14 9.5,19.31C6.73,19.91 6.14,17.97 6.14,17.97C5.68,16.81
|
||||
5.03,16.5 5.03,16.5C4.12,15.88 5.1,15.9 5.1,15.9C6.1,15.97 6.63,16.93 6.63,16.93C7.5,18.45 8.97,18
|
||||
9.54,17.76C9.63,17.11 9.89,16.67 10.17,16.42C7.95,16.17 5.62,15.31 5.62,11.5C5.62,10.39 6,9.5
|
||||
6.65,8.79C6.55,8.54 6.2,7.5 6.75,6.15C6.75,6.15 7.59,5.88 9.5,7.17C10.29,6.95 11.15,6.84 12,6.84C12.85,6.84
|
||||
13.71,6.95 14.5,7.17C16.41,5.88 17.25,6.15 17.25,6.15C17.8,7.5 17.45,8.54 17.35,8.79C18,9.5 18.38,10.39
|
||||
18.38,11.5C18.38,15.32 16.04,16.16 13.81,16.41C14.17,16.72 14.5,17.33 14.5,18.26C14.5,19.6 14.5,20.68
|
||||
14.5,21C14.5,21.27 14.66,21.59 15.17,21.5C19.14,20.16 22,16.42 22,12A10,10 0 0,0 12,2Z" />
|
||||
</svg>;
|
||||
|
||||
const linkTag = (label, { name, url = null, color = 'blue', multiline = false }) => {
|
||||
return (<InternalLink to={url || '/'} className={`tag tag-${color} ${multiline ? 'multiline' : ''}`}>
|
||||
{(name ? <span className="tag-name">{name}</span> : null)}
|
||||
<span className="tag-value">{label}</span>
|
||||
</InternalLink>)
|
||||
}
|
||||
|
||||
const parentTag = (project) => {
|
||||
const membership = Object.values(settings.membership).find(({ crunchbase_and_children }) => {
|
||||
return isParent(crunchbase_and_children, project)
|
||||
});
|
||||
|
||||
if (membership) {
|
||||
const { label, name, crunchbase_and_children } = membership;
|
||||
const slug = crunchbase_and_children.split("/").pop();
|
||||
const url = closeUrl({ grouping: 'organization', filters: {parents: slug}})
|
||||
return linkTag(label, {name, url});
|
||||
}
|
||||
}
|
||||
|
||||
const projectTag = function({relation, isSubsidiaryProject, project, ...item}) {
|
||||
if (relation === false) {
|
||||
return null;
|
||||
}
|
||||
const { prefix, tag } = fields.relation.valuesMap[project] || {};
|
||||
|
||||
if (prefix && tag) {
|
||||
const url = closeUrl({ filters: { relation: project }})
|
||||
return linkTag(tag, {name: prefix, url })
|
||||
}
|
||||
|
||||
if (isSubsidiaryProject) {
|
||||
const url = closeUrl({ mainContentMode: 'card-mode', filters: { relation: 'member', organization: item.organization }})
|
||||
return linkTag("Subsidiary Project", { name: settings.global.short_name, url });
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const memberTag = function({relation, member, enduser}) {
|
||||
if (relation === 'member' || relation === 'company') {
|
||||
const info = settings.membership[member];
|
||||
if (!info) {
|
||||
return null;
|
||||
}
|
||||
const name = info.name;
|
||||
const label = enduser ? (info.end_user_label || info.label) : info.label ;
|
||||
if (!label) {
|
||||
return null;
|
||||
}
|
||||
const url = closeUrl({ filters: { relation }})
|
||||
return linkTag(label, {name: name, url });
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const openSourceTag = function(oss) {
|
||||
if (oss) {
|
||||
const url = closeUrl({ grouping: 'license', filters: {license: 'Open Source'}})
|
||||
return linkTag("Open Source Software", { url, color: "orange" });
|
||||
}
|
||||
};
|
||||
|
||||
const licenseTag = function({relation, license, hideLicense}) {
|
||||
const { label } = _.find(fields.license.values, {id: license});
|
||||
const [width, setWidth] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
const width = CreateWidthMeasurer(window).setFont('0.6rem Roboto');
|
||||
setWidth(width)
|
||||
}, [label])
|
||||
|
||||
if (relation === 'company' || hideLicense) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const url = closeUrl({ grouping: 'license', filters: { license }});
|
||||
return linkTag(label, { name: "License", url, color: "purple", multiline: width > 90 });
|
||||
}
|
||||
const badgeTag = function(itemInfo) {
|
||||
if (settings.global.hide_best_practices) {
|
||||
return null;
|
||||
}
|
||||
if (!itemInfo.bestPracticeBadgeId) {
|
||||
if (itemInfo.oss) {
|
||||
const emptyUrl="https://bestpractices.coreinfrastructure.org/";
|
||||
return (<OutboundLink to={emptyUrl} className="tag tag-grass">
|
||||
<span className="tag-value">No CII Best Practices </span>
|
||||
</OutboundLink>);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
const url = `https://bestpractices.coreinfrastructure.org/en/projects/${itemInfo.bestPracticeBadgeId}`;
|
||||
const label = itemInfo.bestPracticePercentage === 100 ? 'passing' : (itemInfo.bestPracticePercentage + '%');
|
||||
return (<OutboundLink to={url} className="tag tag-grass">
|
||||
<span className="tag-name">CII Best Practices</span>
|
||||
<span className="tag-value">{label}</span>
|
||||
</OutboundLink>);
|
||||
}
|
||||
|
||||
const chart = function(itemInfo) {
|
||||
const { params } = useContext(LandscapeContext)
|
||||
if (params.isEmbed || !itemInfo.github_data || !itemInfo.github_data.languages) {
|
||||
return null;
|
||||
}
|
||||
const callbacks = defaults.plugins.tooltip.callbacks;
|
||||
function percents(v) {
|
||||
const p = Math.round(v / total * 100);
|
||||
if (p === 0) {
|
||||
return '<1%';
|
||||
} else {
|
||||
return p + '%';
|
||||
}
|
||||
}
|
||||
const newCallbacks = { label: function(tooltipItem) {
|
||||
const v = tooltipItem.dataset.data[tooltipItem.dataIndex];
|
||||
const value = millify(v, {precision: 1});
|
||||
const language = languages[tooltipItem.dataIndex];
|
||||
return `${language.name} ${percents(language.value)} (${value})`;
|
||||
}};
|
||||
const allLanguages = itemInfo.github_data.languages;
|
||||
const languages = (function() {
|
||||
const maxEntries = 7;
|
||||
if (allLanguages.length <= maxEntries) {
|
||||
return allLanguages
|
||||
} else {
|
||||
return allLanguages.slice(0, maxEntries).concat([{
|
||||
name: 'Other',
|
||||
value: _.sum( allLanguages.slice(maxEntries - 1).map( (x) => x.value)),
|
||||
color: 'Grey'
|
||||
}]);
|
||||
}
|
||||
})();
|
||||
const data = {
|
||||
labels: languages.map((x) => x.name),
|
||||
datasets: [{
|
||||
data: languages.map( (x) => x.value),
|
||||
backgroundColor: languages.map( (x) => x.color)
|
||||
}]
|
||||
};
|
||||
const total = _.sumBy(languages, 'value');
|
||||
|
||||
function getLegendText(language) {
|
||||
return `${language.name} ${percents(language.value)}`;
|
||||
}
|
||||
|
||||
const legend = <div style={{position: 'absolute', width: 170, left: 0, top: 0, marginTop: -5, marginBottom: 5, fontSize: '0.8em' }}>
|
||||
{languages.map(function(language) {
|
||||
const url = language.name === 'Other' ? null : closeUrl({ grouping: 'no', filters: {language: language.name }});
|
||||
return <div key={language.name} style = {{
|
||||
position: 'relative',
|
||||
marginTop: 2,
|
||||
height: 12
|
||||
}} >
|
||||
<div style={{display: 'inline-block', position: 'absolute', height: 12, width: 12, background: language.color, top: 2, marginRight: 4}} />
|
||||
<div style={{display: 'inline-block', position: 'relative', width: 125, left: 16, whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden'}}>
|
||||
<InternalLink to={url}>{ getLegendText(language) } </InternalLink></div>
|
||||
</div>
|
||||
})}
|
||||
</div>
|
||||
|
||||
return <div style={{width: 220, height: 120, position: 'relative'}}>
|
||||
<div style={{marginLeft: 170, width: 100, height: 100}}>
|
||||
<Pie height={100} width={100} data={data} options={{plugins: {legend: { display: false}, tooltip: {callbacks: newCallbacks}}}} />
|
||||
</div>
|
||||
{ legend }
|
||||
</div>
|
||||
}
|
||||
|
||||
const participation = function(itemInfo) {
|
||||
const { innerWidth } = useWindowSize();
|
||||
const { params } = useContext(LandscapeContext)
|
||||
if (params.isEmbed || !itemInfo.github_data || !itemInfo.github_data.contributions) {
|
||||
return null;
|
||||
}
|
||||
let lastMonth = null;
|
||||
let lastWeek = null;
|
||||
const data = {
|
||||
labels: _.range(0, 51).map(function(week) {
|
||||
const firstWeek = new Date(itemInfo.github_data.firstWeek.replace('Z', 'T00:00:00Z'));
|
||||
firstWeek.setDate(firstWeek.getDate() + week * 7);
|
||||
const m = firstWeek.getMonth();
|
||||
if (lastMonth === null) {
|
||||
lastMonth = m;
|
||||
lastWeek = week;
|
||||
}
|
||||
else if (m % 12 === (lastMonth + 2) % 12) {
|
||||
if (week > lastWeek + 6) {
|
||||
lastMonth = m;
|
||||
lastWeek = week;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
const result = 'Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec'.split(' ')[m];
|
||||
return result;
|
||||
}),
|
||||
datasets: [{
|
||||
backgroundColor: 'darkblue',
|
||||
labels: [],
|
||||
data: itemInfo.github_data.contributions.split(';').map( (x)=> +x).slice(-51)
|
||||
}]
|
||||
};
|
||||
const callbacks = defaults.plugins.tooltip.callbacks;
|
||||
const newCallbacks = { title: function(data) {
|
||||
const firstWeek = new Date(itemInfo.github_data.firstWeek.replace('Z', 'T00:00:00Z'));
|
||||
const week = data[0].dataIndex;
|
||||
firstWeek.setDate(firstWeek.getDate() + week * 7);
|
||||
const s = firstWeek.toISOString().substring(0, 10);
|
||||
return s;
|
||||
}};
|
||||
const options = {
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: {callbacks: newCallbacks}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: { display: false },
|
||||
ticks: {
|
||||
padding: -1,
|
||||
autoSkip: false,
|
||||
minRotation: 0,
|
||||
maxRotation: 0
|
||||
},
|
||||
title: {
|
||||
display: false
|
||||
}
|
||||
},
|
||||
y: {
|
||||
ticks: {
|
||||
beginAtZero: true,
|
||||
callback: function (value) { if (Number.isInteger(value)) { return value; } }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const width = Math.min(innerWidth - 110, 300);
|
||||
return <div style={{width: width, height: 150, position: 'relative'}}>
|
||||
<Bar height={150} width={width} data={data} options={options} />
|
||||
<div style={{
|
||||
transform: 'rotate(-90deg)',
|
||||
position: 'absolute',
|
||||
left: -24,
|
||||
fontSize: 10,
|
||||
top: 59
|
||||
}}>Commits</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function handleUp() {
|
||||
productScrollEl.scrollBy({top: -200, behavior: 'smooth'});
|
||||
}
|
||||
function handleDown() {
|
||||
productScrollEl.scrollBy({top: 200, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
const ItemDialogContent = ({ itemInfo, loading }) => {
|
||||
const { params } = useContext(LandscapeContext)
|
||||
const { onlyModal } = params
|
||||
const [showAllRepos, setShowAllRepos] = useState(false)
|
||||
const { innerWidth, innerHeight } = useWindowSize()
|
||||
|
||||
const linkToOrganization = closeUrl({ grouping: 'organization', filters: {organization: itemInfo.organization}});
|
||||
const itemCategory = function(path) {
|
||||
var separator = <span className="product-category-separator" key="product-category-separator">•</span>;
|
||||
var subcategory = _.find(fields.landscape.values,{id: path});
|
||||
var category = _.find(fields.landscape.values, {id: subcategory.parentId});
|
||||
var categoryMarkup = (
|
||||
<InternalLink key="category" to={closeUrl({ grouping: 'landscape', filters: {landscape: category.id}})}>{category.label}</InternalLink>
|
||||
)
|
||||
var subcategoryMarkup = (
|
||||
<InternalLink key="subcategory" to={closeUrl({ grouping: 'landscape', filters: {landscape: path}})}>{subcategory.label}</InternalLink>
|
||||
)
|
||||
return (<span>{[categoryMarkup, separator, subcategoryMarkup]}</span>);
|
||||
}
|
||||
const twitterElement = itemInfo.twitter &&
|
||||
<div className="product-property row">
|
||||
<div className="product-property-name col col-40">Twitter</div>
|
||||
<div className="product-property-value col col-60">
|
||||
<OutboundLink to={itemInfo.twitter}>{formatTwitter(itemInfo.twitter)}</OutboundLink>
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
const latestTweetDateElement = itemInfo.twitter && (
|
||||
<div className="product-property row">
|
||||
<div className="product-property-name col col-50">Latest Tweet</div>
|
||||
<div className="product-property-value col col-50">
|
||||
{ itemInfo.latestTweetDate && (
|
||||
<OutboundLink to={itemInfo.twitter}>{formatDate(itemInfo.latestTweetDate)}</OutboundLink>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const firstCommitDateElement = itemInfo.firstCommitDate && (
|
||||
<div className="product-property row">
|
||||
<div className="product-property-name col col-40">First Commit</div>
|
||||
<div className="product-property-value tight-col col-60">
|
||||
<OutboundLink to={itemInfo.firstCommitLink} >{formatDate(itemInfo.firstCommitDate)}</OutboundLink>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const contributorsCountElement = itemInfo.contributorsCount ? (
|
||||
<div className="product-property row">
|
||||
<div className="product-property-name col col-40">Contributors</div>
|
||||
<div className="product-property-value tight-col col-60">
|
||||
<OutboundLink to={itemInfo.contributorsLink}>
|
||||
{itemInfo.contributorsCount > 500 ? '500+' : itemInfo.contributorsCount }
|
||||
</OutboundLink>
|
||||
</div>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
const headquartersElement = itemInfo.headquarters && itemInfo.headquarters !== 'N/A' && (
|
||||
<div className="product-property row">
|
||||
<div className="product-property-name col col-40">Headquarters</div>
|
||||
<div className="product-property-value tight-col col-60"><InternalLink to={closeUrl({ grouping: 'headquarters', filters:{headquarters:itemInfo.headquarters}})}>{itemInfo.headquarters}</InternalLink></div>
|
||||
</div>
|
||||
);
|
||||
const amountElement = !loading && !settings.global.hide_funding_and_market_cap && Number.isInteger(itemInfo.amount) && (
|
||||
<div className="product-property row">
|
||||
<div className="product-property-name col col-40">{itemInfo.amountKind === 'funding' ? 'Funding' : 'Market Cap'}</div>
|
||||
{ itemInfo.amountKind === 'funding' &&
|
||||
<div className="product-property-value tight-col col-60">
|
||||
<OutboundLink to={itemInfo.crunchbase + '#section-funding-rounds'}>
|
||||
{'$' + millify(itemInfo.amount)}
|
||||
</OutboundLink>
|
||||
</div>
|
||||
}
|
||||
{ itemInfo.amountKind !== 'funding' &&
|
||||
<div className="product-property-value tight-col col-60">
|
||||
<OutboundLink to={'https://finance.yahoo.com/quote/' + itemInfo.yahoo_finance_data.effective_ticker}>
|
||||
{'$' + millify(itemInfo.amount)}
|
||||
</OutboundLink>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
const tickerElement = itemInfo.ticker && (
|
||||
<div className="product-property row">
|
||||
<div className="product-property-name col col-40">Ticker</div>
|
||||
<div className="product-property-value tight-col col-60">
|
||||
<OutboundLink to={"https://finance.yahoo.com/quote/" + itemInfo.yahoo_finance_data.effective_ticker}>
|
||||
{itemInfo.yahoo_finance_data.effective_ticker}
|
||||
</OutboundLink>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
const latestCommitDateElement = itemInfo.latestCommitDate && (
|
||||
<div className="product-property row">
|
||||
<div className="product-property-name col col-50">Latest Commit</div>
|
||||
<div className="product-property-value col col-50">
|
||||
<OutboundLink to={itemInfo.latestCommitLink}>{formatDate(itemInfo.latestCommitDate)}</OutboundLink>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
const releaseDateElement = itemInfo.releaseDate && (
|
||||
<div className="product-property row">
|
||||
<div className="product-property-name col col-50">Latest Release</div>
|
||||
<div className="product-property-value col col-50">
|
||||
<OutboundLink to={itemInfo.releaseLink}>{formatDate(itemInfo.releaseDate)}</OutboundLink>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
const crunchbaseEmployeesElement = itemInfo.crunchbaseData && itemInfo.crunchbaseData.numEmployeesMin && (
|
||||
<div className="product-property row">
|
||||
<div className="product-property-name col col-50">Headcount</div>
|
||||
<div className="product-property-value col col-50">{formatNumber(itemInfo.crunchbaseData.numEmployeesMin)}-{formatNumber(itemInfo.crunchbaseData.numEmployeesMax)}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const extraElement = ( function() {
|
||||
if (!itemInfo.extra) {
|
||||
return null;
|
||||
}
|
||||
const items = Object.keys(itemInfo.extra).map( function(key) {
|
||||
const value = itemInfo.extra[key];
|
||||
const keyText = (function() {
|
||||
const step1 = key.replace(/_url/g, '');
|
||||
const step2 = step1.split('_').map( (x) => x.charAt(0).toUpperCase() + x.substring(1)).join(' ');
|
||||
return step2;
|
||||
})();
|
||||
const valueText = (function() {
|
||||
if (!!(new Date(value).getTime()) && typeof value === 'string') {
|
||||
return relativeDate(new Date(value));
|
||||
}
|
||||
if (typeof value === 'string' && (value.indexOf('http://') === 0 || value.indexOf('https://') === 0)) {
|
||||
return <OutboundLink to={value}>{value}</OutboundLink>;
|
||||
}
|
||||
return value;
|
||||
})();
|
||||
return <div className="product-property row">
|
||||
<div className="product-property-name tight-col col-20">{keyText}</div>
|
||||
<div className="product-proerty-value tight-col col-80">{valueText}</div>
|
||||
</div>;
|
||||
});
|
||||
return items;
|
||||
})();
|
||||
|
||||
const scrollAllContent = innerWidth < 1000 || innerHeight < 630;
|
||||
const cellStyle = {
|
||||
width: 146,
|
||||
marginRight: 4,
|
||||
height: 26,
|
||||
display: 'inline-block',
|
||||
layout: 'relative',
|
||||
overflow: 'hidden'
|
||||
};
|
||||
|
||||
const productLogoAndTags = <Fragment>
|
||||
<div className="product-logo" style={getRelationStyle(itemInfo.relation)}>
|
||||
<img src={assetPath(itemInfo.href)} className='product-logo-img' alt={itemInfo.name}/>
|
||||
</div>
|
||||
<div className="product-tags">
|
||||
<div className="product-badges" style = {{width: Math.min(300, innerWidth - 110)}} >
|
||||
<div style={cellStyle}>{projectTag(itemInfo)}</div>
|
||||
<div style={cellStyle}>{parentTag(itemInfo)}</div>
|
||||
<div style={cellStyle}>{openSourceTag(itemInfo.oss)}</div>
|
||||
<div style={cellStyle}>{licenseTag(itemInfo)}</div>
|
||||
<div style={cellStyle}>{badgeTag(itemInfo)}</div>
|
||||
<div style={cellStyle}><TweetButton/></div>
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>;
|
||||
|
||||
const charts = <Fragment>
|
||||
{chart(itemInfo)}
|
||||
{participation(itemInfo)}
|
||||
</Fragment>
|
||||
|
||||
const productLogoAndTagsAndCharts = <Fragment>
|
||||
<div className="product-logo" style={getRelationStyle(itemInfo.relation)}>
|
||||
<img src={assetPath(itemInfo.href)} className='product-logo-img'/>
|
||||
</div>
|
||||
<div className="product-tags">
|
||||
<div className="product-badges" style = {{width: 300}} >
|
||||
<div style={cellStyle}>{projectTag(itemInfo)}</div>
|
||||
<div style={cellStyle}>{parentTag(itemInfo)}</div>
|
||||
<div style={cellStyle}>{openSourceTag(itemInfo.oss)}</div>
|
||||
<div style={cellStyle}>{licenseTag(itemInfo)}</div>
|
||||
<div style={cellStyle}>{badgeTag(itemInfo)}</div>
|
||||
<div style={cellStyle}><TweetButton/></div>
|
||||
{chart(itemInfo)}
|
||||
{participation(itemInfo)}
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>;
|
||||
|
||||
const shortenUrl = (url) => url.replace(/http(s)?:\/\/(www\.)?/, "").replace(/\/$/, "");
|
||||
|
||||
const productInfo = <Fragment>
|
||||
<div className="product-main">
|
||||
{ (isGoogle() || onlyModal) ?
|
||||
<React.Fragment>
|
||||
<div className="product-name">{itemInfo.name}</div>
|
||||
<div className="product-description">{itemInfo.description}</div>
|
||||
<div className="product-parent"><InternalLink to={linkToOrganization}>{itemInfo.organization}</InternalLink></div>
|
||||
<div className="product-category">{itemCategory(itemInfo.landscape)}</div>
|
||||
</React.Fragment> :
|
||||
<React.Fragment>
|
||||
<div className="product-name">{itemInfo.name}</div>
|
||||
<div className="product-parent"><InternalLink to={linkToOrganization}><span>{itemInfo.organization}</span>{memberTag(itemInfo)}</InternalLink></div>
|
||||
<div className="product-category">{itemCategory(itemInfo.landscape)}</div>
|
||||
<div className="product-description">{itemInfo.description}</div>
|
||||
</React.Fragment>
|
||||
}
|
||||
</div>
|
||||
{ !loading && <div className="product-properties">
|
||||
<div className="product-property row">
|
||||
<div className="product-property-name col col-20">Website</div>
|
||||
<div className="product-property-value col col-80">
|
||||
<OutboundLink to={itemInfo.homepage_url}>{shortenUrl(itemInfo.homepage_url)}</OutboundLink>
|
||||
</div>
|
||||
</div>
|
||||
{ itemInfo.repos && itemInfo.repos.map(({ url, stars }, idx) => {
|
||||
return <div className={`product-property row ${ idx < 3 || showAllRepos ? '' : 'hidden' }`} key={idx}>
|
||||
<div className="product-property-name col col-20">
|
||||
{ idx === 0 && (itemInfo.repos.length > 1 ? 'Repositories' : 'Repository') }
|
||||
</div>
|
||||
<div className="product-property-value product-repo col col-80">
|
||||
<OutboundLink to={url}>{shortenUrl(url)}</OutboundLink>
|
||||
|
||||
{ idx === 0 && itemInfo.repos.length > 1 && <span className="primary-repo">(primary)</span> }
|
||||
|
||||
{ itemInfo.github_data && <span className="product-repo-stars">
|
||||
<SvgIcon style={{ color: '#7b7b7b' }}>{iconGithub}</SvgIcon>
|
||||
<StarIcon style={{ color: '#7b7b7b' }}/>{formatNumber(stars)}
|
||||
</span> }
|
||||
</div>
|
||||
</div>
|
||||
})}
|
||||
{itemInfo.repos && (itemInfo.repos.length > 3 || (itemInfo.repos.length > 1 && itemInfo.github_data)) &&
|
||||
<div className="product-property row">
|
||||
<div className="product-property-name col col-20"></div>
|
||||
<div className="product-property-value product-repo col col-80">
|
||||
{itemInfo.repos && itemInfo.repos.length > 3 &&
|
||||
<span>
|
||||
<a href="#" onClick={() => setShowAllRepos(!showAllRepos)}>{ showAllRepos ? 'less...' : 'more...' }</a>
|
||||
</span>
|
||||
}
|
||||
{ itemInfo.github_data && <>
|
||||
<span className="product-repo-stars-label">
|
||||
total:
|
||||
</span>
|
||||
<span className="product-repo-stars">
|
||||
<SvgIcon style={{color: '#7b7b7b'}}>{iconGithub}</SvgIcon>
|
||||
<StarIcon style={{color: '#7b7b7b'}} />
|
||||
{formatNumber(itemInfo.github_data.stars)}
|
||||
</span>
|
||||
</>}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{itemInfo.crunchbase &&
|
||||
<div className="product-property row">
|
||||
<div className="product-property-name col col-20">Crunchbase</div>
|
||||
<div className="product-property-value col col-80">
|
||||
<OutboundLink to={itemInfo.crunchbase}>{shortenUrl(itemInfo.crunchbase)}</OutboundLink>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{itemInfo.crunchbaseData && itemInfo.crunchbaseData.linkedin &&
|
||||
<div className="product-property row">
|
||||
<div className="product-property-name col col-20">LinkedIn</div>
|
||||
<div className="product-property-value col col-80">
|
||||
<OutboundLink to={itemInfo.crunchbaseData.linkedin}>
|
||||
{shortenUrl(itemInfo.crunchbaseData.linkedin)}
|
||||
</OutboundLink>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div className="row">
|
||||
{ innerWidth <= 1000 && <div className="col col-50 single-column">
|
||||
{ twitterElement }
|
||||
{ latestTweetDateElement }
|
||||
{ firstCommitDateElement }
|
||||
{ latestCommitDateElement }
|
||||
{ contributorsCountElement }
|
||||
{ releaseDateElement }
|
||||
{ headquartersElement }
|
||||
{ crunchbaseEmployeesElement }
|
||||
{ amountElement }
|
||||
{ tickerElement }
|
||||
</div> }
|
||||
{ innerWidth > 1000 && <div className="col col-50">
|
||||
{ twitterElement }
|
||||
{ firstCommitDateElement }
|
||||
{ contributorsCountElement }
|
||||
{ headquartersElement }
|
||||
{ amountElement }
|
||||
{ tickerElement }
|
||||
</div>
|
||||
}
|
||||
{ innerWidth > 1000 && <div className="col col-50">
|
||||
{ latestTweetDateElement }
|
||||
{ latestCommitDateElement }
|
||||
{ releaseDateElement }
|
||||
{ crunchbaseEmployeesElement }
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
{ extraElement }
|
||||
</div> }
|
||||
</Fragment>;
|
||||
|
||||
return (
|
||||
<div className={classNames("modal-content", {'scroll-all-content': scrollAllContent})} >
|
||||
<Head>
|
||||
<title>{`${itemInfo.name} - ${settings.global.meta.title}`}</title>
|
||||
</Head>
|
||||
|
||||
<KeyHandler keyEventName="keydown" keyValue="ArrowUp" onKeyHandle={handleUp} />
|
||||
<KeyHandler keyEventName="keydown" keyValue="ArrowDown" onKeyHandle={handleDown} />
|
||||
|
||||
{ !scrollAllContent && !isGoogle() && productLogoAndTagsAndCharts }
|
||||
|
||||
<div className="product-scroll" ref={(x) => productScrollEl = x }>
|
||||
{ !scrollAllContent && productInfo }
|
||||
{ scrollAllContent && <div className="landscape-layout">
|
||||
{productLogoAndTags}
|
||||
<div className="right-column">{productInfo}</div>
|
||||
{charts}
|
||||
</div>
|
||||
}
|
||||
|
||||
{ showTwitter && itemInfo.twitter && <TwitterTimeline twitter={itemInfo.twitter} />}
|
||||
</div>
|
||||
{ !scrollAllContent && isGoogle() && productLogoAndTags }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default ItemDialogContent
|
|
@ -0,0 +1,820 @@
|
|||
const _ = require('lodash');
|
||||
const relativeDate = require('relative-date');
|
||||
|
||||
const { formatNumber } = require('../utils/formatNumber');
|
||||
const { isParent } = require('../utils/isParent');
|
||||
const { fields } = require('../types/fields');
|
||||
const { assetPath } = require('../utils/assetPath');
|
||||
const { stringifyParams } = require('../utils/routing');
|
||||
const { millify, h } = require('../utils/format');
|
||||
const icons = require('../utils/icons');
|
||||
|
||||
module.exports.render = function({settings, tweetsCount, itemInfo}) {
|
||||
|
||||
const closeUrl = stringifyParams;
|
||||
|
||||
const formatDate = function(x) {
|
||||
if (x.text) {
|
||||
return x.text;
|
||||
}
|
||||
return relativeDate(new Date(x));
|
||||
};
|
||||
|
||||
function getLinkedIn(itemInfo) {
|
||||
if (itemInfo.extra && itemInfo.extra.override_linked_in) {
|
||||
return itemInfo.extra.override_linked_in;
|
||||
}
|
||||
if (itemInfo.crunchbaseData && itemInfo.crunchbaseData.linkedin) {
|
||||
return itemInfo.crunchbaseData.linkedin;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function getRelationStyle(relation) {
|
||||
const relationInfo = fields.relation.valuesMap[relation]
|
||||
if (relationInfo && relationInfo.color) {
|
||||
return `border: 4px solid ${relationInfo.color};`;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
const formatTwitter = function(x) {
|
||||
const name = x.split('/').slice(-1)[0];
|
||||
return '@' + name;
|
||||
}
|
||||
|
||||
const tweetButton = (function() {
|
||||
// locate zoom buttons
|
||||
|
||||
if (!process.env.TWITTER_KEYS) {
|
||||
return ``
|
||||
}
|
||||
const twitterUrl = `https://twitter.com/intent/tweet`
|
||||
|
||||
return `<div class="tweet-button">
|
||||
<a data-tweet="true" href="${h(twitterUrl)}">${icons.bird}<span>Tweet</span></a>
|
||||
<div class="tweet-count-wrapper">
|
||||
<div class="tweet-count">${tweetsCount}</div>
|
||||
</div>
|
||||
</div>`
|
||||
|
||||
})();
|
||||
|
||||
|
||||
const renderLinkTag = (label, { name, url = null, color = 'blue', multiline = false, twoLines = false }) => {
|
||||
return `<a data-type="internal" href="${url || '/'}" class="tag tag-${color} ${multiline ? 'multiline' : ''} ${twoLines ? 'twolines' : ''}">
|
||||
${(name ? `<span class="tag-name">${h(name)}</span>` : '')}
|
||||
<span class="tag-value">${h(label)}</span>
|
||||
</a>`
|
||||
}
|
||||
|
||||
const renderParentTag = (project) => {
|
||||
const membership = Object.values(settings.membership).find(({ crunchbase_and_children }) => {
|
||||
return isParent(crunchbase_and_children, project)
|
||||
});
|
||||
|
||||
if (membership) {
|
||||
const { label, name, crunchbase_and_children } = membership;
|
||||
const slug = crunchbase_and_children.split("/").pop();
|
||||
const url = closeUrl({ grouping: 'organization', filters: {parents: slug}})
|
||||
return renderLinkTag(label, {name, url});
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
const renderProjectTag = function({relation, isSubsidiaryProject, project, ...item}) {
|
||||
if (relation === false) {
|
||||
return '';
|
||||
}
|
||||
const { prefix, tag } = fields.relation.valuesMap[project] || {};
|
||||
|
||||
if (prefix && tag) {
|
||||
const url = closeUrl({ filters: { relation: project }})
|
||||
return renderLinkTag(tag, {name: prefix, url, twoLines: tag.indexOf(' - ') !== -1 || tag.length > 20 || prefix.length > 20 })
|
||||
}
|
||||
|
||||
if (isSubsidiaryProject) {
|
||||
const url = closeUrl({ filters: { relation: 'member', organization: item.organization }})
|
||||
return renderLinkTag("Subsidiary Project", { name: settings.global.short_name, url });
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const renderMemberTag = function({relation, member, enduser}) {
|
||||
if (relation === 'member' || relation === 'company') {
|
||||
const info = settings.membership[member];
|
||||
if (!info) {
|
||||
return '';
|
||||
}
|
||||
const name = info.name;
|
||||
const label = enduser ? (info.end_user_label || info.label) : info.label ;
|
||||
if (!label) {
|
||||
return '';
|
||||
}
|
||||
const url = closeUrl({ filters: { relation }})
|
||||
return renderLinkTag(label, {name: name, url });
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
const renderOpenSourceTag = function(oss) {
|
||||
if (oss) {
|
||||
const url = closeUrl({ grouping: 'license', filters: {license: 'Open Source'}})
|
||||
return renderLinkTag("Open Source Software", { url, color: "orange" });
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const renderLicenseTag = function({relation, license, hideLicense, extra}) {
|
||||
const { label } = _.find(fields.license.values, {id: license});
|
||||
|
||||
if (extra && extra.hide_license) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (relation === 'company' || hideLicense) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const url = closeUrl({ grouping: 'license', filters: { license }});
|
||||
return renderLinkTag(label, { name: "License", url, color: "purple", multiline: true});
|
||||
}
|
||||
|
||||
const renderBadgeTag = function() {
|
||||
if (settings.global.hide_best_practices) {
|
||||
return '';
|
||||
}
|
||||
if (!itemInfo.bestPracticeBadgeId) {
|
||||
if (settings.global.hide_no_best_practices) {
|
||||
return '';
|
||||
}
|
||||
if (itemInfo.oss) {
|
||||
const emptyUrl="https://bestpractices.coreinfrastructure.org/";
|
||||
return `<a data-type="external" target="_blank" href=${emptyUrl} class="tag tag-grass">
|
||||
<span class="tag-value">No OpenSSF Best Practices </span>
|
||||
</a>`;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
const url = `https://bestpractices.coreinfrastructure.org/en/projects/${itemInfo.bestPracticeBadgeId}`;
|
||||
const label = itemInfo.bestPracticePercentage === 100 ? '✓' : (itemInfo.bestPracticePercentage + '%');
|
||||
return (`<a data-type="external" target="_blank" href="${url}" class="tag tag-grass">
|
||||
<span class="tag-name">OpenSSF Best Practices</span>
|
||||
<span class="tag-value">${label}</span>
|
||||
</a>`);
|
||||
}
|
||||
|
||||
const renderChart = function() {
|
||||
if (!itemInfo.github_data || !itemInfo.github_data.languages) {
|
||||
return '';
|
||||
}
|
||||
const allLanguages = itemInfo.github_data.languages;
|
||||
const languages = (function() {
|
||||
const maxEntries = 7;
|
||||
if (allLanguages.length <= maxEntries) {
|
||||
return allLanguages
|
||||
} else {
|
||||
return allLanguages.slice(0, maxEntries).concat([{
|
||||
name: 'Other',
|
||||
value: _.sum( allLanguages.slice(maxEntries - 1).map( (x) => x.value)),
|
||||
color: 'Grey'
|
||||
}]);
|
||||
}
|
||||
})();
|
||||
function getLegendText(language) {
|
||||
const total = _.sumBy(languages, 'value');
|
||||
function percents(v) {
|
||||
const p = Math.round(v / total * 100);
|
||||
if (p === 0) {
|
||||
return '<1%';
|
||||
} else {
|
||||
return p + '%';
|
||||
}
|
||||
}
|
||||
return `${language.name} ${percents(language.value)}`;
|
||||
}
|
||||
|
||||
const legend = `
|
||||
<div style="
|
||||
position: absolute;
|
||||
width: 170px;
|
||||
left: 0;
|
||||
top: 0;
|
||||
margin-top: -5px;
|
||||
margin-bottom: 5px;
|
||||
font-size: 0.8em;
|
||||
">
|
||||
${languages.map(function(language) {
|
||||
const url = language.name === 'Other' ? null : closeUrl({ grouping: 'no', filters: {language: language.name }});
|
||||
return `<div style="position: relative; margin-top: 2px; height: 12px;" >
|
||||
<div style="display: inline-block; position: absolute; height: 12px; width: 12px; background: ${language.color}; top: 2px; margin-right: 4px;" ></div>
|
||||
<div style="display: inline-block; position: relative; width: 125px; left: 16px; white-space: nowrap; text-overflow: 'ellipsis'; overflow: hidden;">
|
||||
<a data-type="internal" href="${url}">${h(getLegendText(language)) }</a></div>
|
||||
</div>`
|
||||
}).join('')}
|
||||
</div> `;
|
||||
|
||||
|
||||
// a quick 50 lines pie chart implementation is here
|
||||
const renderSector = ({
|
||||
path, fill
|
||||
}) => `
|
||||
<path
|
||||
d="${path}"
|
||||
fill="${fill}"
|
||||
stroke="#fff"
|
||||
strokeWidth="1"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
`;
|
||||
|
||||
const renderCircle = ({
|
||||
center, color, radius
|
||||
}) => `
|
||||
<ellipse cx=${center} cy=${center} fill=${color} rx=${radius} ry=${radius} stroke="#fff" strokeWidth="1" ></ellipse>
|
||||
`;
|
||||
|
||||
const renderSectors = ({
|
||||
center,
|
||||
data
|
||||
}) => {
|
||||
const total = data.reduce((prev, current) => current.value + prev, 0)
|
||||
let angleStart = -90;
|
||||
let angleEnd = -90;
|
||||
let angleMargin = 0;
|
||||
return total > 0 ? `
|
||||
<g>
|
||||
${data.map((d) => {
|
||||
const isLarge = d.value / total > 0.5;
|
||||
const angle = 360 * d.value / total;
|
||||
const radius = center - 1 / 2;
|
||||
|
||||
angleStart = angleEnd;
|
||||
angleMargin = angleMargin > angle ? angle : angleMargin;
|
||||
angleEnd = angleStart + angle - angleMargin;
|
||||
|
||||
const x1 = center + radius * Math.cos(Math.PI * angleStart / 180);
|
||||
const y1 = center + radius * Math.sin(Math.PI * angleStart / 180);
|
||||
const x2 = center + radius * Math.cos(Math.PI * angleEnd / 180);
|
||||
const y2 = center + radius * Math.sin(Math.PI * angleEnd / 180);
|
||||
const path = `
|
||||
M${center},${center}
|
||||
L${x1},${y1}
|
||||
A${radius},${radius}
|
||||
0 ${isLarge ? 1 : 0},1
|
||||
${x2},${y2}
|
||||
z
|
||||
`
|
||||
angleEnd += angleMargin;
|
||||
return renderSector({fill: d.color, path: path});
|
||||
}).join('')}
|
||||
</g>
|
||||
` : ''
|
||||
}
|
||||
|
||||
const renderPie = ({data}) => {
|
||||
const viewBoxSize = 100;
|
||||
const center = viewBoxSize / 2;
|
||||
if (!data || data.length === 0) {
|
||||
return '';
|
||||
}
|
||||
return `<svg viewBox="0 0 ${viewBoxSize } ${viewBoxSize}">
|
||||
<g>
|
||||
${ data.length === 1
|
||||
? renderCircle({center: center, radius: center, ...data[0]})
|
||||
: renderSectors({center: center, data: data})
|
||||
}
|
||||
</g>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
return `<div style="width: 220px; height: 120px; position: relative">
|
||||
<div style="margin-left: 170px; width: 100px; height: 100px;">
|
||||
${renderPie({data: languages})}
|
||||
</div>
|
||||
${legend}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const renderParticipation = function() {
|
||||
if (!itemInfo.github_data || !itemInfo.github_data.contributions) {
|
||||
return '';
|
||||
}
|
||||
// build an Y scale axis
|
||||
// build an X scale axis
|
||||
const monthText = (function() {
|
||||
const firstWeek = new Date(itemInfo.github_data.firstWeek.replace('Z', 'T00:00:00Z'));
|
||||
const months = 'Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec'.split(' ');
|
||||
const result = [];
|
||||
const m = firstWeek.getMonth();
|
||||
for (let i = 0; i < 12; i += 2) {
|
||||
const monthName = months[(m + i) % 12];
|
||||
const separator = i === 12 ? '' : `<span style="width: 23px; display: inline-block;" ></span>`;
|
||||
result.push(`<span style="width: 30px; display: inline-block">${monthName}</span>`);
|
||||
result.push(separator);
|
||||
}
|
||||
return result.join('');
|
||||
})();
|
||||
|
||||
const barValues = itemInfo.github_data.contributions.split(';').map( (x)=> +x).slice(-51)
|
||||
const { maxValue, step } = ( () => {
|
||||
const max = _.max(barValues);
|
||||
let maxValue;
|
||||
let step;
|
||||
for (let pow = 0; pow < 10; pow++) {
|
||||
for (let v of [1, 2, 5]) {
|
||||
const value = v * Math.pow(10, pow);
|
||||
if (value >= max && !maxValue) {
|
||||
maxValue = value;
|
||||
if (pow === 0) {
|
||||
step = v;
|
||||
} else {
|
||||
step = 5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
step,
|
||||
maxValue
|
||||
}
|
||||
})();
|
||||
const xyLines = ( () => {
|
||||
const result = []
|
||||
for (let x = 0; x <= step; x += 1) {
|
||||
result.push(`<div style="
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
right: 0;
|
||||
top: ${(x / step) * 100}%;
|
||||
height: .5px;
|
||||
background: #777;"
|
||||
></div>
|
||||
`)
|
||||
result.push(`<span style="
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
font-size: 10px;
|
||||
left: 5px;
|
||||
right: 0px;
|
||||
top: ${(x / step) * 150 - 7}px;
|
||||
">${(step - x) / step * maxValue}</span>`);
|
||||
}
|
||||
result.push(`<div style="
|
||||
position: absolute;
|
||||
left: 25px;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
width: .5px;
|
||||
background: #777;
|
||||
"></div>`);
|
||||
return result.join('');
|
||||
})();
|
||||
const bars = barValues.map(function(value, index) {
|
||||
if (value === 0) {
|
||||
value = 1;
|
||||
}
|
||||
return `<div style="
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
top: ${(maxValue - value) / maxValue * 150}px;
|
||||
left: ${24 + index * 5.6}px;
|
||||
width: 4px;
|
||||
background: #00F;
|
||||
border: 1px solid #777;
|
||||
" ></div>`;
|
||||
}).join('');
|
||||
|
||||
|
||||
const width = 300;
|
||||
return `<div style="width: ${width}px; height: 150px; position: relative;">
|
||||
${xyLines}
|
||||
${bars}
|
||||
<div style="
|
||||
transform: rotate(-90deg);
|
||||
position: absolute;
|
||||
left: -24px;
|
||||
font-size: 10px;
|
||||
top: 59px;
|
||||
">Commits</div>
|
||||
<div style="
|
||||
font-size: 10px;
|
||||
left: 20px;
|
||||
bottom: -16px;
|
||||
white-space: nowrap;
|
||||
position: absolute;
|
||||
">${monthText}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const linkToOrganization = closeUrl({ grouping: 'organization', filters: {organization: itemInfo.organization}});
|
||||
|
||||
const renderItemCategory = function({path, itemInfo}) {
|
||||
var separator = `<span class="product-category-separator" key="product-category-separator">•</span>`;
|
||||
var subcategory = _.find(fields.landscape.values,{id: path});
|
||||
if (!subcategory) {
|
||||
throw new Error(`Failed to render ${itemInfo.name}, can not find a subcategory: ${path}, available paths are below: \n${fields.landscape.values.map( (x) => x.id).join('\n')}`);
|
||||
}
|
||||
var category = _.find(fields.landscape.values, {id: subcategory.parentId});
|
||||
var categoryMarkup = `
|
||||
<a data-type="internal" href="${closeUrl({ grouping: 'landscape', filters: {landscape: category.id}})}">${h(category.label)}</a>
|
||||
`
|
||||
var subcategoryMarkup = `
|
||||
<a data-type="internal" href="${closeUrl({ grouping: 'landscape', filters: {landscape: path}})}">${h(subcategory.label)}</a>
|
||||
`
|
||||
return `<span>${categoryMarkup} ${separator} ${subcategoryMarkup}</span>`;
|
||||
}
|
||||
|
||||
const twitterElement = itemInfo.twitter ? `
|
||||
<div class="product-property row">
|
||||
<div class="product-property-name col col-40">Twitter</div>
|
||||
<div class="product-property-value col col-60">
|
||||
<a data-type="external" target="_blank" href="${itemInfo.twitter}">${h(formatTwitter(itemInfo.twitter))}</a>
|
||||
</div>
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
const latestTweetDateElement = itemInfo.twitter ? `
|
||||
<div class="product-property row">
|
||||
<div class="product-property-name col col-50">Latest Tweet</div>
|
||||
<div class="product-property-value col col-50">
|
||||
${ itemInfo.latestTweetDate ? `
|
||||
<a data-type="external" target="_blank" href="${h(itemInfo.twitter)}">${formatDate(itemInfo.latestTweetDate)}</a>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
const firstCommitDateElement = itemInfo.firstCommitDate ? `
|
||||
<div class="product-property row">
|
||||
<div class="product-property-name col col-40">First Commit</div>
|
||||
<div class="product-property-value tight-col col-60">
|
||||
<a data-type="external" target=_blank href="${h(itemInfo.firstCommitLink)}">${formatDate(itemInfo.firstCommitDate)}</a>
|
||||
</div>
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
const contributorsCountElement = itemInfo.contributorsCount ? `
|
||||
<div class="product-property row">
|
||||
<div class="product-property-name col col-40">Contributors</div>
|
||||
<div class="product-property-value tight-col col-60">
|
||||
<a data-type="external" target=_blank href="${itemInfo.contributorsLink}">
|
||||
${itemInfo.contributorsCount > 500 ? '500+' : itemInfo.contributorsCount }
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
const headquartersElement = itemInfo.headquarters && itemInfo.headquarters !== 'N/A' ? `
|
||||
<div class="product-property row">
|
||||
<div class="product-property-name col col-40">Headquarters</div>
|
||||
<div class="product-property-value tight-col col-60">
|
||||
<a data-type="external" target=_blank href="${closeUrl({ grouping: 'headquarters', filters:{headquarters:itemInfo.headquarters}})}">${h(itemInfo.headquarters)}</a>
|
||||
</div>
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
const amountElement = !settings.global.hide_funding_and_market_cap && Number.isInteger(itemInfo.amount) ? `
|
||||
<div class="product-property row">
|
||||
<div class="product-property-name col col-40">${itemInfo.amountKind === 'funding' ? 'Funding' : 'Market Cap'}</div>
|
||||
${ itemInfo.amountKind === 'funding' ? `
|
||||
<div class="product-property-value tight-col col-60">
|
||||
<a data-type="external" target=_blank href="${itemInfo.crunchbase + '#section-funding-rounds'}">
|
||||
${'$' + millify(itemInfo.amount)}
|
||||
</a>
|
||||
</div>` : ''
|
||||
}
|
||||
${ itemInfo.amountKind !== 'funding' ? `
|
||||
<div class="product-property-value tight-col col-60">
|
||||
<a data-type="external" target=_blank href="https://finance.yahoo.com/quote/${itemInfo.yahoo_finance_data.effective_ticker}">
|
||||
${'$' + millify(itemInfo.amount)}
|
||||
</a>
|
||||
</div>` : ''
|
||||
}
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
const tickerElement = itemInfo.ticker ? `
|
||||
<div class="product-property row">
|
||||
<div class="product-property-name col col-40">Ticker</div>
|
||||
<div class="product-property-value tight-col col-60">
|
||||
<a data-type="external" target=_blank href="https://finance.yahoo.com/quote/${itemInfo.yahoo_finance_data.effective_ticker}">
|
||||
${h(itemInfo.yahoo_finance_data.effective_ticker)}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
const latestCommitDateElement = itemInfo.latestCommitDate ? `
|
||||
<div class="product-property row">
|
||||
<div class="product-property-name col col-50">Latest Commit</div>
|
||||
<div class="product-property-value col col-50">
|
||||
<a data-type="external" target=_blank href="${itemInfo.latestCommitLink}">${formatDate(itemInfo.latestCommitDate)}</a>
|
||||
</div>
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
const releaseDateElement = itemInfo.releaseDate ? `
|
||||
<div class="product-property row">
|
||||
<div class="product-property-name col col-50">Latest Release</div>
|
||||
<div class="product-property-value col col-50">
|
||||
<a data-type="external" target=_blank href="${itemInfo.releaseLink}">${formatDate(itemInfo.releaseDate)}</a>
|
||||
</div>
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
const crunchbaseEmployeesElement = itemInfo.crunchbaseData && itemInfo.crunchbaseData.numEmployeesMin ? `
|
||||
<div class="product-property row">
|
||||
<div class="product-property-name col col-50">Headcount</div>
|
||||
<div class="product-property-value col col-50">${formatNumber(itemInfo.crunchbaseData.numEmployeesMin)}-${formatNumber(itemInfo.crunchbaseData.numEmployeesMax)}</div>
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
const specialDates = ( function() {
|
||||
let specialKeys = ['accepted', 'incubation', 'graduated', 'archived'];
|
||||
const names = {
|
||||
accepted: 'Accepted',
|
||||
incubation: 'Incubation',
|
||||
graduated: 'Graduated',
|
||||
archived: 'Archived'
|
||||
}
|
||||
let result = {};
|
||||
for (let key of specialKeys) {
|
||||
if (itemInfo.extra && itemInfo.extra[key]) {
|
||||
result[key] = itemInfo.extra[key];
|
||||
delete itemInfo.extra[key];
|
||||
}
|
||||
}
|
||||
|
||||
const keys = Object.keys(result);
|
||||
const values = Object.values(result);
|
||||
if (keys.length === 0) {
|
||||
return '';
|
||||
}
|
||||
if (keys.length === 1) {
|
||||
return `
|
||||
<div class="product-property row">
|
||||
<div class="product-property-name col col-20">${names[keys[0]]}</div>
|
||||
<div class="product-property-name col col-80">${values[0]}</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
if (keys.length === 2) {
|
||||
return `
|
||||
<div class="row">
|
||||
<div class="col col-50">
|
||||
<div class="product-property row">
|
||||
<div class="product-property-name col col-40">${names[keys[0]]}</div>
|
||||
<div class="product-property-name col col-60">${values[0]}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-50">
|
||||
<div class="product-property row">
|
||||
<div class="product-property-name col col-50">${names[keys[1]]}</div>
|
||||
<div class="product-property-name col col-50">${values[1]}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
if (keys.length === 3) {
|
||||
return `
|
||||
<div class="row">
|
||||
<div class="col col-50">
|
||||
<div class="product-property row">
|
||||
<div class="product-property-name col col-40">${names[keys[0]]}</div>
|
||||
<div class="product-property-name col col-60">${values[0]}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-50">
|
||||
<div class="product-property row">
|
||||
<div class="product-property-name col col-50">${names[keys[1]]}</div>
|
||||
<div class="product-property-name col col-50">${values[1]}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="product-property row">
|
||||
<div class="product-property-name col col-20">${names[keys[2]]}</div>
|
||||
<div class="product-property-name col col-80">${values[2]}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
return '';
|
||||
})();
|
||||
|
||||
const cloElement = ( function() {
|
||||
if (!itemInfo.extra) {
|
||||
return '';
|
||||
}
|
||||
if (!itemInfo.extra.clomonitor_svg) {
|
||||
return '';
|
||||
}
|
||||
return `
|
||||
<a href="https://clomonitor.io/projects/cncf/${itemInfo.extra.clomonitor_name}" target="_blank">
|
||||
${itemInfo.extra.clomonitor_svg}
|
||||
</a>
|
||||
`;
|
||||
})();
|
||||
|
||||
const extraElement = ( function() {
|
||||
if (!itemInfo.extra) {
|
||||
return '';
|
||||
}
|
||||
const items = Object.keys(itemInfo.extra).map( function(key) {
|
||||
if (key.indexOf('summary_') === 0) {
|
||||
return '';
|
||||
}
|
||||
if (key === 'clomonitor_name' || key === 'clomonitor_svg') {
|
||||
return '';
|
||||
}
|
||||
if (key === 'hide_license') {
|
||||
return '';
|
||||
}
|
||||
if (key === 'override_linked_in') {
|
||||
return '';
|
||||
}
|
||||
if (key === 'audits') {
|
||||
const value = itemInfo.extra[key];
|
||||
const lines = (value.map ? value : [value]).map( (auditInfo) => `
|
||||
<div>
|
||||
<a href="${h(auditInfo.url)}" target="_blank">${h(auditInfo.type)} at ${auditInfo.date}</a>
|
||||
</div>
|
||||
`).join('');
|
||||
return `<div class="product-property row">
|
||||
<div class="product-property-name tight-col col-20">Audits</div>
|
||||
<div class="product-proerty-value tight-col col-80">${lines}</div>
|
||||
</div>`;
|
||||
}
|
||||
const value = itemInfo.extra[key];
|
||||
const keyText = (function() {
|
||||
const step1 = key.replace(/_url/g, '');
|
||||
const step2 = step1.split('_').map( (x) => x.charAt(0).toUpperCase() + x.substring(1)).join(' ');
|
||||
return step2;
|
||||
})();
|
||||
const valueText = (function() {
|
||||
if (!!(new Date(value).getTime()) && typeof value === 'string') {
|
||||
return h(relativeDate(new Date(value)));
|
||||
}
|
||||
if (typeof value === 'string' && (value.indexOf('http://') === 0 || value.indexOf('https://') === 0)) {
|
||||
return `<a data-type="external" target=_blank href="${h(value)}">${h(value)}</a>`;
|
||||
}
|
||||
return h(value);
|
||||
})();
|
||||
return `<div class="product-property row">
|
||||
<div class="product-property-name tight-col col-20">${h(keyText)}</div>
|
||||
<div class="product-proerty-value tight-col col-80">${valueText}</div>
|
||||
</div>`;
|
||||
});
|
||||
return items.join('');
|
||||
})();
|
||||
|
||||
const cellStyle = `
|
||||
width: 146px;
|
||||
marginRight: 4px;
|
||||
height: 26px;
|
||||
display: inline-block;
|
||||
layout: relative;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const productLogoAndTagsAndCharts = `
|
||||
<div class="product-logo" style="${getRelationStyle(itemInfo.relation)}">
|
||||
<img alt="product logo" src="${assetPath(itemInfo.href)}" class="product-logo-img">
|
||||
</div>
|
||||
<div class="product-tags">
|
||||
<div class="product-badges" style="width: 300px;" >
|
||||
<div style="${cellStyle}">${renderProjectTag(itemInfo)}</div>
|
||||
<div style="${cellStyle}">${renderParentTag(itemInfo)}</div>
|
||||
<div style="${cellStyle}">${renderOpenSourceTag(itemInfo.oss)}</div>
|
||||
<div style="${cellStyle}">${renderLicenseTag(itemInfo)}</div>
|
||||
<div style="${cellStyle}">${renderBadgeTag(itemInfo)}</div>
|
||||
<div style="${cellStyle}">${tweetButton}</div>
|
||||
<div class="charts-desktop">
|
||||
${renderChart(itemInfo)}
|
||||
${renderParticipation(itemInfo)}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
|
||||
const shortenUrl = (url) => url.replace(/http(s)?:\/\/(www\.)?/, "").replace(/\/$/, "");
|
||||
|
||||
const productPaths1 = [itemInfo.landscape, itemInfo.second_path || [], itemInfo.allPaths || []].flat();
|
||||
const productPaths = _.uniq(productPaths1.filter( (x) => !!x));
|
||||
const productInfo = `
|
||||
<div class="product-main">
|
||||
<div class="product-name">${h(itemInfo.name)}</div>
|
||||
<div class="product-parent"><a data-type=internal href="${linkToOrganization}">
|
||||
<span>${h(itemInfo.organization)}</span>${renderMemberTag(itemInfo)}</a></div>
|
||||
${productPaths.map( (productPath) => `
|
||||
<div class="product-category">${renderItemCategory({path: productPath, itemInfo})}</div>
|
||||
`).join('')}
|
||||
<div class="product-description">${h(itemInfo.description)}</div>
|
||||
</div>
|
||||
<div class="product-properties">
|
||||
<div class="product-property row">
|
||||
<div class="product-property-name col col-20">Website</div>
|
||||
<div class="product-property-value col col-80">
|
||||
<a data-type=external target=_blank href="${itemInfo.homepage_url}">${shortenUrl(itemInfo.homepage_url)}</a>
|
||||
</div>
|
||||
</div>
|
||||
${ (itemInfo.repos || []).map(({ url, stars }, idx) => {
|
||||
return `<div class="product-property row">
|
||||
<div class="product-property-name col col-20">
|
||||
${ idx === 0 ? (itemInfo.repos.length > 1 ? 'Repositories' : 'Repository') : '' }
|
||||
</div>
|
||||
<div class="product-property-value product-repo col col-80">
|
||||
<a data-type=external target=_blank href="${url}">${shortenUrl(url)}</a>
|
||||
${ idx === 0 && itemInfo.repos.length > 1 ? `<span class="primary-repo">(primary)</span>` : '' }
|
||||
${ itemInfo.github_data ? `<span class="product-repo-stars">
|
||||
${icons.github}
|
||||
${icons.star}
|
||||
${formatNumber(stars)}
|
||||
</span> ` : ''
|
||||
}
|
||||
</div>
|
||||
</div>`
|
||||
}).join('')}
|
||||
${itemInfo.repos && (itemInfo.repos.length > 3 || (itemInfo.repos.length > 1 && itemInfo.github_data)) ? `
|
||||
<div class="product-property row">
|
||||
<div class="product-property-name col col-20"></div>
|
||||
<div class="product-property-value product-repo col col-80">
|
||||
${ itemInfo.github_data ? `
|
||||
<span class="product-repo-stars-label">
|
||||
total:
|
||||
</span>
|
||||
<span class="product-repo-stars">
|
||||
${icons.github}
|
||||
${icons.star}
|
||||
${formatNumber(itemInfo.github_data.stars)}
|
||||
</span> ` : ''
|
||||
}
|
||||
</div>
|
||||
</div> ` : ''
|
||||
}
|
||||
${itemInfo.crunchbase ? `
|
||||
<div class="product-property row">
|
||||
<div class="product-property-name col col-20">Crunchbase</div>
|
||||
<div class="product-property-value col col-80">
|
||||
<a data-type=external target=_blank href="${itemInfo.crunchbase}">${shortenUrl(itemInfo.crunchbase)}</a>
|
||||
</div>
|
||||
</div> ` : ''
|
||||
}
|
||||
${getLinkedIn(itemInfo) ? `
|
||||
<div class="product-property row">
|
||||
<div class="product-property-name col col-20">LinkedIn</div>
|
||||
<div class="product-property-value col col-80">
|
||||
<a data-type=external target=_blank href="${getLinkedIn(itemInfo)}">
|
||||
${shortenUrl(getLinkedIn(itemInfo))}
|
||||
</a>
|
||||
</div>
|
||||
</div> ` : ''
|
||||
}
|
||||
<div class="row">
|
||||
<div class="col col-50">
|
||||
${ twitterElement }
|
||||
${ firstCommitDateElement }
|
||||
${ contributorsCountElement }
|
||||
</div>
|
||||
<div class="col col-50">
|
||||
${ latestTweetDateElement }
|
||||
${ latestCommitDateElement }
|
||||
${ releaseDateElement }
|
||||
</div>
|
||||
</div>
|
||||
${specialDates}
|
||||
<div class="row">
|
||||
<div class="col col-50">
|
||||
${ headquartersElement }
|
||||
${ amountElement }
|
||||
${ tickerElement }
|
||||
</div>
|
||||
<div class="col col-50">
|
||||
${ crunchbaseEmployeesElement }
|
||||
</div>
|
||||
</div>
|
||||
${extraElement}
|
||||
${cloElement}
|
||||
</div>
|
||||
`;
|
||||
|
||||
const result = `<div class="modal-content ${itemInfo.oss ? 'oss' : 'nonoss'}">
|
||||
${productLogoAndTagsAndCharts}
|
||||
<div class="product-scroll" >
|
||||
${productInfo}
|
||||
<div class="charts-mobile">
|
||||
${renderChart(itemInfo)}
|
||||
${renderParticipation(itemInfo)}
|
||||
</div>
|
||||
${ itemInfo.twitter ? `<div class="twitter-timeline">
|
||||
<a class="twitter-timeline" aria-hidden="true" data-tweet-limit="5" href="${itemInfo.twitter}"></a>
|
||||
</div>` : '' }
|
||||
</div>
|
||||
</div>`;
|
||||
return result;
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
const _ = require('lodash');
|
||||
|
||||
// Render all items here!
|
||||
|
||||
const { renderHorizontalCategory } = require('./HorizontalCategory');
|
||||
const { renderVerticalCategory } = require('./VerticalCategory');
|
||||
const { renderLandscapeInfo } = require('./LandscapeInfo');
|
||||
const { renderOtherLandscapeLink } = require('./OtherLandscapeLink');
|
||||
|
||||
const extractKeys = (obj, keys) => {
|
||||
const attributes = _.pick(obj, keys)
|
||||
|
||||
return _.mapKeys(attributes, (value, key) => _.camelCase(key))
|
||||
}
|
||||
|
||||
|
||||
module.exports.render = function({landscapeSettings, landscapeItems}) {
|
||||
const elements = landscapeSettings.elements.map(element => {
|
||||
if (element.type === 'LandscapeLink') {
|
||||
return renderOtherLandscapeLink(element)
|
||||
}
|
||||
if (element.type === 'LandscapeInfo') {
|
||||
return renderLandscapeInfo(element)
|
||||
}
|
||||
const category = landscapeItems.find(c => c.key === element.category);
|
||||
if (!category) {
|
||||
console.info(`Can not find the ${element.category}`);
|
||||
console.info(`Valid values: ${landscapeItems.map( (x) => x.key).join('; ')}`);
|
||||
}
|
||||
const attributes = extractKeys(element, ['width', 'height', 'top', 'left', 'color', 'fit_width', 'is_large'])
|
||||
const subcategories = category.subcategories.map(subcategory => {
|
||||
const allItems = subcategory.allItems.map(item => ({ ...item, categoryAttrs: attributes }))
|
||||
return { ...subcategory, allItems }
|
||||
})
|
||||
|
||||
if (element.type === 'HorizontalCategory') {
|
||||
return renderHorizontalCategory({...category, ...attributes, subcategories: subcategories});
|
||||
}
|
||||
if (element.type === 'VerticalCategory') {
|
||||
return renderVerticalCategory({...category, ...attributes, subcategories: subcategories});
|
||||
}
|
||||
}).join('');
|
||||
|
||||
return `<div style="position: relative;">${elements}</div>`;
|
||||
};
|
|
@ -0,0 +1,56 @@
|
|||
const { h } = require('../utils/format');
|
||||
const { assetPath } = require('../utils/assetPath');
|
||||
|
||||
module.exports.renderLandscapeInfo = function({width, height, top, left, children}) {
|
||||
children = children.map(function(info) {
|
||||
const positionStyle = `
|
||||
position: absolute;
|
||||
top: ${info.top}px;
|
||||
left: ${info.left}px;
|
||||
right: ${info.right}px;
|
||||
bottom: ${info.bottom}px;
|
||||
width: ${info.width}px;
|
||||
height: ${info.height}px;
|
||||
`;
|
||||
if (info.type === 'text') {
|
||||
return `<div key='text' style="
|
||||
${positionStyle}
|
||||
font-size: ${info.font_size * 4}px;
|
||||
font-style: italic;
|
||||
text-align: justify;
|
||||
z-index: 1;
|
||||
"><div style="
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 400%;
|
||||
height: 100%;
|
||||
transform: scale(0.25);
|
||||
transform-origin: left;
|
||||
"> ${h(info.text)} </div></div>`;
|
||||
}
|
||||
if (info.type === 'title') {
|
||||
return `<div key='title' style="
|
||||
${positionStyle}
|
||||
font-size: ${info.font_size}px;
|
||||
color: #666;
|
||||
">${h(info.title)}</div>`;
|
||||
}
|
||||
if (info.type === 'image') {
|
||||
return `<img src="${assetPath(`images/${info.image}`)}" style="${positionStyle}" alt="${info.title || info.image}" />`;
|
||||
}
|
||||
}).join('');
|
||||
|
||||
return `<div style="
|
||||
position: absolute;
|
||||
width: ${width}px;
|
||||
height: ${height - 20}px;
|
||||
top: ${top}px;
|
||||
left: ${left}px;
|
||||
border: 1px solid black;
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
margin-top: 20px;
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
|
||||
">${children}</div>`
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
import React from 'react'
|
||||
import InternalLink from './InternalLink'
|
||||
import assetPath from '../utils/assetPath'
|
||||
import settings from 'public/settings.json';
|
||||
|
||||
const LandscapeLogo = () => {
|
||||
const { name } = settings.global
|
||||
|
||||
return <span className="landscape-logo">
|
||||
<InternalLink to="/">
|
||||
<img src={assetPath("/images/left-logo.svg")} alt={name}/>
|
||||
</InternalLink>
|
||||
</span>
|
||||
}
|
||||
|
||||
export default LandscapeLogo
|
|
@ -1,14 +0,0 @@
|
|||
import { useContext } from 'react'
|
||||
import TreeSelector from './TreeSelector';
|
||||
import { options } from '../types/fields';
|
||||
import LandscapeContext from '../contexts/LandscapeContext'
|
||||
|
||||
const LicenseFilterContainer = () => {
|
||||
const { navigate, params } = useContext(LandscapeContext)
|
||||
const value = params.filters.license
|
||||
const _options = options('license')
|
||||
const onChange = license => navigate({ filters: { license }})
|
||||
return <TreeSelector onChange={onChange} value={value} options={_options} />
|
||||
}
|
||||
|
||||
export default LicenseFilterContainer
|
|
@ -1,174 +0,0 @@
|
|||
import React, { useContext, useEffect, useState, useRef } from 'react';
|
||||
import StarIcon from '@material-ui/icons/Star';
|
||||
import millify from 'millify'
|
||||
import classNames from 'classnames'
|
||||
import ListSubheader from '@material-ui/core/ListSubheader';
|
||||
import _ from 'lodash';
|
||||
import InternalLink from './InternalLink';
|
||||
import fields from '../types/fields';
|
||||
import LandscapeContext from '../contexts/LandscapeContext'
|
||||
import assetPath from '../utils/assetPath'
|
||||
import { useRouter } from 'next/router'
|
||||
import { stringifyParams } from '../utils/routing'
|
||||
import useCurrentDevice from '../utils/useCurrentDevice'
|
||||
|
||||
function getRelationStyle(relation) {
|
||||
const relationInfo = fields.relation.valuesMap[relation]
|
||||
if (relationInfo && relationInfo.color) {
|
||||
return {
|
||||
border: '4px solid ' + relationInfo.color
|
||||
};
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
const Card = ({item, handler, ...props}) => {
|
||||
const { params } = useContext(LandscapeContext)
|
||||
const { cardStyle } = params
|
||||
if (cardStyle === 'flat') {
|
||||
return FlatCard({item, handler, ...props});
|
||||
} else if (cardStyle === 'borderless') {
|
||||
return BorderlessCard({item, handler, ...props});
|
||||
} else {
|
||||
return DefaultCard({item, handler, ...props});
|
||||
}
|
||||
}
|
||||
|
||||
const DefaultCard = ({item, handler, itemRef, ...props}) => {
|
||||
return (
|
||||
<div ref={itemRef} className="mosaic-wrap" key={item.id} {...props}>
|
||||
<div className={classNames('mosaic', {nonoss : item.oss === false})} style={getRelationStyle(item.relation)}
|
||||
onClick={() => handler(item.id)} >
|
||||
<div className="logo_wrapper">
|
||||
<img src={assetPath(item.href)} className='logo' max-height='100%' max-width='100%' alt={item.name} />
|
||||
</div>
|
||||
<div className="mosaic-info">
|
||||
<div className="mosaic-title">
|
||||
<h5>{item.name}</h5>
|
||||
{item.organization}
|
||||
</div>
|
||||
<div className="mosaic-stars">
|
||||
{ _.isNumber(item.stars) && item.stars &&
|
||||
<div>
|
||||
<StarIcon color="disabled" style={{ fontSize: 15 }}/>
|
||||
<span style={{position: 'relative', top: -3}}>{item.starsAsText}</span>
|
||||
</div>
|
||||
}
|
||||
{ Number.isInteger(item.amount) &&
|
||||
<div className="mosaic-funding">{item.amountKind === 'funding' ? 'Funding: ': 'MCap: '} {'$'+ millify( item.amount )}</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const FlatCard = function({item, handler, itemRef, ...props}) {
|
||||
return (
|
||||
<div ref={itemRef} className="mosaic-wrap" key={item.id} {...props}>
|
||||
<div className="mosaic" onClick={() => handler(item.id)} >
|
||||
<img src={assetPath(item.href)} className='logo' alt={item.name} />
|
||||
<div className="separator"/>
|
||||
<h5>{item.flatName}</h5>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const BorderlessCard = function({item, handler, itemRef, ...props}) {
|
||||
return (
|
||||
<div className="mosaic-wrap" key={item.id} {...props}>
|
||||
<div className="mosaic" onClick={() => handler(item.id)} >
|
||||
<img src={assetPath(item.href)} className='logo' alt={item.name} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const Header = ({groupedItem, ...props}) => {
|
||||
return (
|
||||
<div className="sh_wrapper" key={"subheader:" + groupedItem.header} {...props}>
|
||||
<ListSubheader component="div" style={{fontSize: 24, paddingLeft: 16 }}>
|
||||
{ groupedItem.href ? <InternalLink to={groupedItem.href}>{groupedItem.header}</InternalLink> : <span>{groupedItem.header}</span> }
|
||||
<span className="items-count"> ({groupedItem.items.length})</span></ListSubheader>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
That is quite a complex component. It draws headers and cards, and also animates the difference
|
||||
- previous list of items is remembered every time, lately referenced as 'old'
|
||||
- we scroll to the top after every change, so we need to animate only those items which are at the start
|
||||
- if a card was visible with previous parameters and is visible with current parameters, we apply a 'move' animation
|
||||
- otherwise, old items fade out and new items fade in
|
||||
- for performance, we draw first 30 items with animations, and delay rendering of the remaining parts to provide a quicker response to the user
|
||||
- those 30 items are just an estimation, we calculate weather a card or a header are really visible in the current viewport or not
|
||||
*/
|
||||
|
||||
|
||||
|
||||
const MainContent = () => {
|
||||
const { navigate, params, groupedItems } = useContext(LandscapeContext)
|
||||
const { cardStyle, isEmbed } = params
|
||||
const loader = useRef(null)
|
||||
const totalItems = groupedItems.reduce((sum, group) => sum + group.items.length, 0)
|
||||
const [maxItems, setMaxItems] = useState(isEmbed ? totalItems : 100)
|
||||
const { asPath } = useRouter()
|
||||
const currentDevice = useCurrentDevice()
|
||||
|
||||
useEffect(() => {
|
||||
const options = { root: null, rootMargin: '0px', threshold: 1.0 }
|
||||
|
||||
const callback = (entries, _) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting && maxItems < totalItems) {
|
||||
setMaxItems(maxItems + 100)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(callback, options);
|
||||
|
||||
observer.observe(loader.current)
|
||||
|
||||
return () => observer.disconnect()
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setMaxItems(isEmbed ? totalItems : 100)
|
||||
}, [asPath])
|
||||
|
||||
const handler = selectedItemId => {
|
||||
if (currentDevice.mobile() && isEmbed) {
|
||||
const url = stringifyParams({ ...params, selectedItemId })
|
||||
window.open(url,'_blank')
|
||||
} else {
|
||||
navigate({ selectedItemId }, { scroll: false })
|
||||
}
|
||||
}
|
||||
|
||||
let itemsCount = 0
|
||||
|
||||
const itemsAndHeaders = groupedItems.flatMap(groupedItem => {
|
||||
const items = groupedItem.items.slice(0, maxItems - itemsCount)
|
||||
|
||||
itemsCount += items.length
|
||||
|
||||
const cards = items.map(item => <Card key={item.id} item={item} handler={handler}/>)
|
||||
return [
|
||||
items.length > 0 ? <Header key={groupedItem.href} groupedItem={groupedItem} /> : null,
|
||||
...cards
|
||||
]
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={classNames('column-content', {[cardStyle + '-mode']: true})}>
|
||||
{ itemsAndHeaders }
|
||||
<div ref={loader} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MainContent;
|
|
@ -1,9 +0,0 @@
|
|||
import React from 'react';
|
||||
import { pure } from 'recompose';
|
||||
|
||||
const Note = () => {
|
||||
return <div className="sidebar-note">
|
||||
Greyed logos are not open source
|
||||
</div>
|
||||
}
|
||||
export default pure(Note);
|
|
@ -0,0 +1,78 @@
|
|||
const _ = require('lodash');
|
||||
const { h } = require('../utils/format');
|
||||
const l = function(x) {
|
||||
return h((x || "").replace("https://", ""));
|
||||
}
|
||||
const { formatNumber } = require('../utils/formatNumber');
|
||||
function highlightLinks(s) {
|
||||
if (!s) {
|
||||
return '';
|
||||
}
|
||||
// markdown styles
|
||||
s = s.replace(/\[(.*?)\]\((https?:.*?)\)/g, '<a target="_blank" href="$2">$1</a>')
|
||||
s = s.replace(/(\s|^)(https?:.*?)(\s|$)/g, ' <a target="_blank" href="$2">$2</a> ')
|
||||
return s;
|
||||
}
|
||||
const getDate = function(date) {
|
||||
if (!date) {
|
||||
return '';
|
||||
}
|
||||
return new Date(date).toISOString().substring(0, 10);
|
||||
}
|
||||
const today = getDate(new Date());
|
||||
|
||||
module.exports.render = function({items}) {
|
||||
console.info(items[0], items[0].latestCommitDate);
|
||||
|
||||
const old = _.orderBy( items.filter( (x) => x.latestCommitDate && new Date(x.latestCommitDate).getTime() + 3 * 30 * 86400 * 1000 < new Date().getTime()), 'latestCommitDate');
|
||||
|
||||
console.info(old.map( (x) => x.name));
|
||||
|
||||
return `
|
||||
<head>
|
||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||
<style>
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>List of obsolete items</h1>
|
||||
${old.map(function(item) {
|
||||
return `
|
||||
<div style="display: flex; flex-direction: row;">
|
||||
<div style="width: 200px; overflow: hidden; padding: 5px;">
|
||||
<img src="logos/${item.image_data.fileName}"></img>
|
||||
</div>
|
||||
<div style="width: 300px; overflow: hidden; padding: 5px;">
|
||||
<h3>${item.name}</h1>
|
||||
<h3>${item.path}</h2>
|
||||
<h3><span><b>Latest Commit: </b></span> ${getDate(item.latestCommitDate)}</h3>
|
||||
<h3><span><b>Repo: </b></span> ${highlightLinks(item.repo_url)}</h3>
|
||||
</div>
|
||||
<div style="width: 600px; padding: 5px;">
|
||||
<h4>Manual Actions</h4>
|
||||
<pre>
|
||||
1. Create an issue in their repo, inform that that a repo is scheduled for deletion.
|
||||
2. Mark this item in a <b>landscape.yml</b> with an <b>extra</b> property <b>obsolete_since</b> equal to ${today}
|
||||
3. After a month remove the entry from the repo
|
||||
===
|
||||
Issue Template:
|
||||
Title: ${item.name} is going to be unreferenced from the interactive landscape because of no activity since ${getDate(item.latestCommitDate)}
|
||||
Body:
|
||||
<div style="font-size: 8px;">
|
||||
Dear project maintainers of ${item.name},
|
||||
|
||||
I hope this message finds you well.
|
||||
I noticed that your project has had no activity for the last 3 months and is about to be removed from the interactive landscape https://landscape.cncf.io/?selected=${item.id}
|
||||
As a maintainer of an interactive landscape, we have included your project as a reference for our users and would like to ensure that the information we provide is up-to-date.
|
||||
We understand that maintaining a project can be challenging and time-consuming, and we would like to offer any assistance that we can. Please let us know if there are any plans to continue the development of the project or if there is anything we can do to help.
|
||||
Thank you for your time and efforts in creating and maintaining this project. We appreciate the value it has provided to the community and hope to continue to reference it in our interactive landscape.
|
||||
Best regards, CNCF Landscape Team
|
||||
</div>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('<hr>')}
|
||||
</body>
|
||||
`
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
import ComboboxMultiSelector from './ComboboxMultiSelector';
|
||||
import { options } from '../types/fields';
|
||||
import { useContext } from 'react'
|
||||
import LandscapeContext from '../contexts/LandscapeContext'
|
||||
|
||||
const OrganizationFilterContainer = () => {
|
||||
const { navigate, params } = useContext(LandscapeContext)
|
||||
const value = params.filters.organization
|
||||
const _options = options('organization')
|
||||
const onChange = organization => navigate({ filters: { organization } })
|
||||
return <ComboboxMultiSelector onChange={onChange} value={value} options={_options} />
|
||||
}
|
||||
|
||||
export default OrganizationFilterContainer
|
|
@ -0,0 +1,97 @@
|
|||
const { assetPath } = require('../utils/assetPath');
|
||||
const { h } = require('../utils/format');
|
||||
|
||||
const { stringifyParams } = require('../utils/routing');
|
||||
const { categoryBorder, categoryTitleHeight, subcategoryTitleHeight } = require('../utils/landscapeCalculations');
|
||||
|
||||
const renderCardLink = ({ url, children }) => {
|
||||
if (url.indexOf('http') === 0) {
|
||||
return `<a data-type=external target=_blank href="${url}" style="display: flex; flex-direction: column;">${children}</a>`;
|
||||
} else {
|
||||
url = stringifyParams({ mainContentMode: url });
|
||||
return `<a data-type=tab href="${url}" style="display: flex; flex-direction: column;">${children}</a>`;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.renderOtherLandscapeLink = function({top, left, height, width, color, title, image, url, layout}) {
|
||||
title = title || ''; //avoid undefined!
|
||||
const imageSrc = image || assetPath(`images/${url}_preview.png`);
|
||||
if (layout === 'category') {
|
||||
return `<div style="
|
||||
position: absolute;
|
||||
top: ${top}px;
|
||||
left: ${left}px;
|
||||
height: ${height}px;
|
||||
width: ${width}px;
|
||||
background: ${color};
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
|
||||
padding: 1px;
|
||||
display: flex;
|
||||
">
|
||||
${renderCardLink({url: url, children: `
|
||||
<div style="width: ${width}px;height: 30px; line-height: 28px; text-align: center; color: white; font-size: 12px;">${h(title)}</div>
|
||||
<div style="flex: 1; background: white; position: relative; display: flex; justify-content: center; align-items: center;">
|
||||
<img loading="lazy" src="${imageSrc}" style="
|
||||
width: ${width - 12}px;
|
||||
height: ${height - 42}px;
|
||||
object-fit: contain;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;" alt="${h(title)}" />
|
||||
</div>`})}
|
||||
</div>`;
|
||||
}
|
||||
if (layout === 'subcategory') {
|
||||
return `<div style="
|
||||
width: ${width}px;
|
||||
left: ${left}px;
|
||||
height: ${height}px;
|
||||
top: ${top}px;
|
||||
position: absolute;
|
||||
overflow: hidden;">
|
||||
${renderCardLink({url: url, children: `
|
||||
<div
|
||||
style="
|
||||
position: absolute;
|
||||
background: ${color};
|
||||
top: ${subcategoryTitleHeight}px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
boxShadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.2);
|
||||
padding: ${categoryBorder}px;
|
||||
display: flex;
|
||||
"
|
||||
>
|
||||
<div style="
|
||||
width: ${categoryTitleHeight}px;
|
||||
writing-mode: vertical-rl;
|
||||
transform: rotate(180deg);
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
line-height: 13px;
|
||||
color: white;
|
||||
">
|
||||
${h(title)}
|
||||
</div>
|
||||
<div style="
|
||||
display: flex;
|
||||
flex: 1;
|
||||
background: white;
|
||||
justify-content: center;
|
||||
align-items: center; ">
|
||||
<img loading="lazy" src="${imageSrc}" alt="${h(title)}"
|
||||
style="width: ${width - 42}px;
|
||||
height: ${height - 32}px;
|
||||
object-fit: contain;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;" />
|
||||
</div>
|
||||
</div>`})}
|
||||
</div>`
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
import React from 'react';
|
||||
import { OutboundLink } from 'react-ga';
|
||||
|
||||
const OverrideOutboundLink = ({to, eventLabel, children, ...props}) => {
|
||||
return (
|
||||
<OutboundLink to={to} eventLabel={eventLabel || to} target="_blank" rel="noopener noreferrer" {...props}>
|
||||
{children}
|
||||
</OutboundLink>
|
||||
)
|
||||
}
|
||||
|
||||
export default OverrideOutboundLink
|
|
@ -1,56 +0,0 @@
|
|||
import React from 'react'
|
||||
import { parse } from 'query-string'
|
||||
import { useRouter } from 'next/router'
|
||||
import { pure } from 'recompose'
|
||||
import InternalLink from './InternalLink'
|
||||
import convertLegacyUrl from '../utils/convertLegacyUrl'
|
||||
import settings from 'public/settings.json'
|
||||
|
||||
const queriesMatch = (query, otherQuery) => {
|
||||
const params = parse(query)
|
||||
const otherParams = parse(otherQuery)
|
||||
|
||||
for (const key in params) {
|
||||
if (params[key] !== otherParams[key]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const urlsMatch = (url, otherUrl) => {
|
||||
const [path, query] = url.split('?')
|
||||
const [otherPath, otherQuery] = otherUrl.split('?')
|
||||
|
||||
if (path !== otherPath) {
|
||||
return false
|
||||
}
|
||||
|
||||
return queriesMatch(query, otherQuery)
|
||||
}
|
||||
|
||||
const presets = settings.presets.map(preset => {
|
||||
const url = preset.url.indexOf('=') >= 0 ? convertLegacyUrl(preset.url) : preset.url
|
||||
return { ...preset, url: url[0] === '/' ? url : `/${url}` }
|
||||
})
|
||||
|
||||
const Preset = ({ preset }) => {
|
||||
const router = useRouter()
|
||||
const active = urlsMatch(preset.url, router.asPath)
|
||||
|
||||
return <div>
|
||||
<InternalLink className={`preset ${active ? 'active' : null}`} to={preset.url}>
|
||||
{preset.label}
|
||||
</InternalLink>
|
||||
</div>
|
||||
}
|
||||
|
||||
const Presets = () => {
|
||||
return <div className="sidebar-presets">
|
||||
<h4>Example filters:</h4>
|
||||
{presets.map(preset => <Preset key={preset.url} preset={preset} />)}
|
||||
</div>
|
||||
}
|
||||
|
||||
export default pure(Presets);
|
|
@ -1,14 +0,0 @@
|
|||
import TreeSelector from './TreeSelector';
|
||||
import { options } from '../types/fields';
|
||||
import { useContext } from 'react'
|
||||
import LandscapeContext from '../contexts/LandscapeContext'
|
||||
|
||||
const ProjectFilterContainer = () => {
|
||||
const { navigate, params } = useContext(LandscapeContext)
|
||||
const value = params.filters.relation
|
||||
const _options = options('relation')
|
||||
const onChange = relation => navigate({ filters: { relation }})
|
||||
return <TreeSelector onChange={onChange} options={_options} value={value} />
|
||||
}
|
||||
|
||||
export default ProjectFilterContainer
|
|
@ -1,24 +0,0 @@
|
|||
import React from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import FormControlLabel from '@material-ui/core/FormControlLabel';
|
||||
import Radio from '@material-ui/core/Radio';
|
||||
import RadioGroup from '@material-ui/core/RadioGroup';
|
||||
|
||||
const idToValue = (id) => id !== null ? id : 'any';
|
||||
const valueToId = (value) => value === 'any' ? null : value;
|
||||
|
||||
const RadioSelector = ({value, options, onChange}) => {
|
||||
return <RadioGroup name="stars"
|
||||
value={idToValue(value)}
|
||||
>
|
||||
{ options.map( (entry) => (
|
||||
<FormControlLabel
|
||||
key={idToValue(entry.id)}
|
||||
value={idToValue(entry.id)}
|
||||
control={<Radio onClick={() => onChange(valueToId(entry.id))} />}
|
||||
label={entry.label}
|
||||
/>
|
||||
)) }
|
||||
</RadioGroup>
|
||||
};
|
||||
export default pure(RadioSelector);
|
|
@ -1,24 +0,0 @@
|
|||
import React, { useContext } from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import ResetIcon from '@material-ui/icons/SettingsBackupRestore';
|
||||
import LandscapeContext from '../contexts/LandscapeContext'
|
||||
import { stringifyParams } from '../utils/routing'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
const ResetFilters = _ => {
|
||||
const { params } = useContext(LandscapeContext)
|
||||
const router = useRouter()
|
||||
|
||||
// TODO: clean up with validate
|
||||
const reset = _ => {
|
||||
const url = stringifyParams({ ...params, filters: null })
|
||||
router.push(url)
|
||||
}
|
||||
|
||||
return (
|
||||
<a className="filters-action" onClick={reset} aria-label="Reset Filters">
|
||||
<ResetIcon /><span>Reset Filters</span>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
export default pure(ResetFilters);
|
|
@ -1,17 +0,0 @@
|
|||
import { useContext } from 'react'
|
||||
import { sortOptions } from '../types/fields'
|
||||
import SortFieldSelector from './SortFieldSelector'
|
||||
import LandscapeContext from '../contexts/LandscapeContext'
|
||||
|
||||
const SortFieldContainer = () => {
|
||||
const { navigate, params } = useContext(LandscapeContext)
|
||||
const isBigPicture = params.mainContentMode !== 'card-mode'
|
||||
const value = params.sortField
|
||||
const options = sortOptions.filter(field => !field.disabled)
|
||||
|
||||
const onChange = sortField => navigate({ sortField })
|
||||
|
||||
return <SortFieldSelector isBigPicture={isBigPicture} value={value} onChange={onChange} options={options} />
|
||||
}
|
||||
|
||||
export default SortFieldContainer
|
|
@ -1,25 +0,0 @@
|
|||
import React from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
import Select from '@material-ui/core/Select';
|
||||
import ComboboxSelector from './ComboboxSelector';
|
||||
|
||||
|
||||
const SortFieldSelector = ({isBigPicture, value, options, onChange}) => {
|
||||
if (!isBigPicture) {
|
||||
return <ComboboxSelector value={value} options={options} onChange={onChange} />;
|
||||
} else {
|
||||
return (
|
||||
<Select
|
||||
disabled
|
||||
value="empty"
|
||||
style={{width:175 ,fontSize:'0.8em'}}
|
||||
>
|
||||
<MenuItem value="empty">
|
||||
<em>N/A</em>
|
||||
</MenuItem>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
};
|
||||
export default pure(SortFieldSelector);
|
|
@ -1,15 +0,0 @@
|
|||
import React from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import FormGroup from '@material-ui/core/FormGroup';
|
||||
import FormControl from '@material-ui/core/FormControl';
|
||||
import FormLabel from '@material-ui/core/FormLabel';
|
||||
import SortFieldContainer from './SortFieldContainer';
|
||||
const Sorting = () => {
|
||||
return <FormGroup row>
|
||||
<FormControl component="fieldset">
|
||||
<FormLabel component="legend">Sort By</FormLabel>
|
||||
<SortFieldContainer />
|
||||
</FormControl>
|
||||
</FormGroup>;
|
||||
};
|
||||
export default pure(Sorting);
|
|
@ -1,30 +1,16 @@
|
|||
import GuideLink from './GuideLink'
|
||||
import css from 'styled-jsx/css'
|
||||
import { smallItemHeight, smallItemWidth } from '../utils/landscapeCalculations'
|
||||
const { renderGuideLink } = require('./GuideLink');
|
||||
const { smallItemHeight, smallItemWidth } = require('../utils/landscapeCalculations');
|
||||
|
||||
|
||||
const SubcategoryInfo = ({ label, anchor, row, column }) => {
|
||||
const base = css.resolve`
|
||||
module.exports.renderSubcategoryInfo = function({ label, anchor, row, column }) {
|
||||
const style=`
|
||||
width: ${smallItemWidth}px;
|
||||
height: ${smallItemHeight}px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
`
|
||||
|
||||
const extra = css.resolve`
|
||||
grid-column-start: ${column || 'auto'};
|
||||
grid-row-start: ${row || 'auto'};
|
||||
`
|
||||
|
||||
const className = `${base.className} ${extra.className}`
|
||||
|
||||
return <>
|
||||
{base.styles}
|
||||
{extra.styles}
|
||||
<GuideLink className={className} label={label} anchor={anchor}/>
|
||||
</>
|
||||
`;
|
||||
return renderGuideLink({label: label, anchor: anchor, style: style})
|
||||
}
|
||||
|
||||
export default SubcategoryInfo
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
import React, { useContext } from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import millify from 'millify';
|
||||
import formatNumber from '../utils/formatNumber';
|
||||
import _ from 'lodash';
|
||||
import LandscapeContext from '../contexts/LandscapeContext'
|
||||
import getSummary from '../utils/summaryCalculator'
|
||||
|
||||
const getText = ({summary}) => {
|
||||
if (!summary.total) {
|
||||
return 'There are no cards matching your filters';
|
||||
}
|
||||
const cardsText = summary.total === 1 ? 'card' : 'cards';
|
||||
const startText = `You are viewing ${formatNumber(summary.total)} ${cardsText} with a total`;
|
||||
const starsSection = summary.stars ? `of ${formatNumber(summary.stars)} stars` : null;
|
||||
const marketCapSection = summary.marketCap ? `market cap of $${millify(summary.marketCap)}` : null;
|
||||
const fundingSection = summary.funding ? `funding of $${millify(summary.funding)}` : null;
|
||||
if (!marketCapSection && !fundingSection && !starsSection) {
|
||||
return `You are viewing ${formatNumber(summary.total)} ${cardsText}.`;
|
||||
}
|
||||
|
||||
const parts = [starsSection, marketCapSection, fundingSection].filter( (x) => !!x);
|
||||
const startPartsText = _.slice(parts, 0, -1).join(', ');
|
||||
const lastPart = _.slice(parts, -1)[0];
|
||||
const text = [startPartsText, lastPart].filter( (x) => !!x).join(' and ');
|
||||
return `${startText} ${text}.`;
|
||||
}
|
||||
|
||||
const Summary = _ => {
|
||||
const { entries, params } = useContext(LandscapeContext)
|
||||
const summary = getSummary(params, entries)
|
||||
|
||||
return <h4 className="summary">{getText({summary})}</h4>;
|
||||
}
|
||||
export default pure(Summary);
|
|
@ -0,0 +1,624 @@
|
|||
const _ = require('lodash');
|
||||
const { h } = require('../utils/format');
|
||||
const l = function(x) {
|
||||
return h((x || "").replace("https://", ""));
|
||||
}
|
||||
const { formatNumber } = require('../utils/formatNumber');
|
||||
|
||||
const getLanguages = function(item) {
|
||||
if (item.extra && item.extra.summary_languages) {
|
||||
return item.extra.summary_languages;
|
||||
}
|
||||
if (item.github_data && item.github_data.languages) {
|
||||
const total = _.sum(item.github_data.languages.map( (x) => x.value));
|
||||
const matching = item.github_data.languages.filter( (x) => x.value > total * 0.3).map( (x) => x.name);
|
||||
return matching.join(', ');
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function highlightLinks(s) {
|
||||
if (!s) {
|
||||
return '';
|
||||
}
|
||||
// markdown styles
|
||||
s = s.replace(/\[(.*?)\]\((https?:.*?)\)/g, '<a target="_blank" href="$2">$1</a>')
|
||||
s = s.replace(/(\s|^)(https?:.*?)(\s|$)/g, ' <a target="_blank" href="$2">$2</a> ')
|
||||
return s;
|
||||
}
|
||||
|
||||
const getDate = function(date) {
|
||||
if (!date) {
|
||||
return '';
|
||||
}
|
||||
return new Date(date).toISOString().substring(0, 10);
|
||||
}
|
||||
|
||||
module.exports.render = function({items}) {
|
||||
|
||||
const projects = items.filter( (x) => !!x.relation && x.relation !== 'member');
|
||||
const categories = _.uniq(projects.map( (x) => x.path.split(' / ')[0]));
|
||||
const categoriesCount = {};
|
||||
const categoryItems = {};
|
||||
const subcategories = {};
|
||||
for (let k of categories) {
|
||||
categoriesCount[k] = projects.filter( (x) => x.path.split(' / ')[0] === k).length;
|
||||
categoryItems[k] = projects.filter( (x) => x.path.split(' / ')[0] === k).map( (x) => projects.indexOf(x));
|
||||
const arr = _.uniq(projects.filter( (x) => x.path.split(' / ')[0] === k).map( (x) => x.path.split(' / ')[1]));
|
||||
for (let subcategory of arr) {
|
||||
categoryItems[k + ':' + subcategory] = projects.filter( (x) => x.path === k + ' / ' + subcategory).map( (x) => projects.indexOf(x));
|
||||
}
|
||||
subcategories[k] = arr;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
const columnWidth = 250;
|
||||
|
||||
return `
|
||||
<head>
|
||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||
<style>
|
||||
${require('fs').readFileSync('src/fonts.css', 'utf-8')}
|
||||
::root {
|
||||
--navy: #38404a;
|
||||
--navy-light: #696D70;
|
||||
--blue: #2E67BF;
|
||||
--blue-hover: #1D456B;
|
||||
--spacing: 1em;
|
||||
}
|
||||
body {
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
font-family: "Roboto", "Helvetica", "Arial", sans-serif;
|
||||
font-weight: 400;
|
||||
line-height: 1.43;
|
||||
letter-spacing: 0.01071em;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
td a {
|
||||
text-decoration: none;
|
||||
color: #2E67BF;
|
||||
}
|
||||
|
||||
.category {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.categories {
|
||||
display: inline-block;
|
||||
margin: 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.subcategories {
|
||||
display: inline-block;
|
||||
margin: 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: ${columnWidth * projects.length}px;
|
||||
table-layout: fixed;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
border-top: 1px solid white;
|
||||
}
|
||||
|
||||
#headers {
|
||||
width: 152px;
|
||||
position: sticky;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
#data {
|
||||
position: absolute;
|
||||
left: 155px;
|
||||
top: 0px;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
white-space: pre-wrap;
|
||||
width: ${columnWidth}px;
|
||||
margin: 0;
|
||||
border: 1px solid white;
|
||||
border-top-width: 0px;
|
||||
height: 53px;
|
||||
padding: 5px;
|
||||
overflow: hidden;
|
||||
font-size: 0.8em;
|
||||
color: var(--navy);
|
||||
}
|
||||
|
||||
.project-name {
|
||||
text-align: center;
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
|
||||
.table-wrapper {
|
||||
position: absolute;
|
||||
top: 165px;
|
||||
width: calc(100% - 16px);
|
||||
bottom: 0;
|
||||
overflow-x: scroll;
|
||||
overflow-y: scroll;
|
||||
padding: 0;
|
||||
left: 16px;
|
||||
}
|
||||
|
||||
.sticky {
|
||||
position: relative;
|
||||
background-color: #1b446c;
|
||||
color: white;
|
||||
width: 152px;
|
||||
top: auto;
|
||||
font-size: 0.8em;
|
||||
font-weight: bold;
|
||||
padding: 0px;
|
||||
}
|
||||
.first-line {
|
||||
background-color: #1b446c;
|
||||
color: white;
|
||||
}
|
||||
.alternate-line {
|
||||
background-color: #e5e5e5;
|
||||
}
|
||||
.sticky span {
|
||||
white-space: nowrap;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.1rem;
|
||||
line-height: 30px;
|
||||
display: block;
|
||||
margin: 0 0 14px;
|
||||
color: var(--navy);
|
||||
}
|
||||
.landscape-logo {
|
||||
width: 160px;
|
||||
height: 48px;
|
||||
display: inline-block;
|
||||
}
|
||||
.landscapeapp-logo {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 14px;
|
||||
}
|
||||
.landscapeapp-logo img {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
|
||||
.main-header {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
/* select starting stylings ------------------------------*/
|
||||
.select {
|
||||
font-family: 'Roboto';
|
||||
position: relative;
|
||||
width: 240px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.select-disabled {
|
||||
opacity: 0.35;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.select-text {
|
||||
position: relative;
|
||||
font-family: inherit;
|
||||
background-color: transparent;
|
||||
width: 100%;
|
||||
font-size: 11px;
|
||||
padding: 10px 22px 10px 0;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
border-bottom: 1px solid rgba(0,0,0, 0.12);
|
||||
}
|
||||
|
||||
/* Remove focus */
|
||||
.select-text:focus {
|
||||
outline: none;
|
||||
border-bottom: 1px solid rgba(0,0,0, 0);
|
||||
}
|
||||
|
||||
/* Use custom arrow */
|
||||
.select .select-text {
|
||||
appearance: none;
|
||||
-webkit-appearance:none
|
||||
}
|
||||
|
||||
.select:after {
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
right: 10px;
|
||||
/* Styling the down arrow */
|
||||
width: 0;
|
||||
height: 0;
|
||||
padding: 0;
|
||||
content: '';
|
||||
border-left: 6px solid transparent;
|
||||
border-right: 6px solid transparent;
|
||||
border-top: 6px solid rgba(0, 0, 0, 0.12);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
||||
/* LABEL ======================================= */
|
||||
.select-label {
|
||||
color: rgb(105, 109, 112);
|
||||
font-size: 10px;
|
||||
font-weight: normal;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
left: 0;
|
||||
top: 10px;
|
||||
transition: 0.2s ease all;
|
||||
}
|
||||
|
||||
/* active state */
|
||||
.select-text:focus ~ .select-label, .select-text ~ .select-label {
|
||||
top: -10px;
|
||||
transition: 0.2s ease all;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
/* BOTTOM BARS ================================= */
|
||||
.select-bar {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.select-bar:before, .select-bar:after {
|
||||
content: '';
|
||||
height: 2px;
|
||||
width: 0;
|
||||
bottom: 1px;
|
||||
position: absolute;
|
||||
background: #2F80ED;
|
||||
transition: 0.2s ease all;
|
||||
}
|
||||
|
||||
.select-bar:before {
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
.select-bar:after {
|
||||
right: 50%;
|
||||
}
|
||||
|
||||
/* active state */
|
||||
.select-text:focus ~ .select-bar:before, .select-text:focus ~ .select-bar:after {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.select-highlight {
|
||||
position: absolute;
|
||||
height: 60%;
|
||||
width: 100px;
|
||||
top: 25%;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.info {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 540px;
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="main-header">
|
||||
<span class="landscape-logo">
|
||||
<a aria-label="reset filters" class="nav-link" href="/">
|
||||
<img alt="landscape logo" src="/images/left-logo.svg">
|
||||
</a>
|
||||
</span>
|
||||
|
||||
<span style="display: inline-block; position: relative; top: -8px; left: 20px;">
|
||||
<h1>CNCF Project Summary Table</h1>
|
||||
</span>
|
||||
|
||||
<a rel="noopener noreferrer noopener noreferrer" class="landscapeapp-logo" title="CNCF" target="_blank" href="https://www.cncf.io">
|
||||
<img src="/images/right-logo.svg" title="CNCF">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div style="padding: 16px; position: relative; top: -19px;">
|
||||
<div class="categories">
|
||||
<div class="select">
|
||||
<select class="select-text" required="">
|
||||
<option value="" selected="">All: ${projects.length}</option>
|
||||
${categories.map( (name) => `<option value="${name}">${name}: ${categoriesCount[name]}</option>`).join('')}
|
||||
</select>
|
||||
<span class="select-highlight"></span>
|
||||
<span class="select-bar"></span>
|
||||
<label class="select-label">Category</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="subcategories" style="display: none">
|
||||
<div class="select">
|
||||
<select class="select-text" required="">
|
||||
|
||||
</select>
|
||||
<span class="select-highlight"></span>
|
||||
<span class="select-bar"></span>
|
||||
<label class="select-label">Subcategory</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info">
|
||||
The <i>CNCF Project Summary Table</i> provides a standardized, summary of CNCF projects.<br/><div style="height: 5px;"></div>
|
||||
<b style="color: rgb(58,132,247);">The filters on the left side help refine your view.</b> Start by filtering by category (e.g., <i>orchestration and management</i>) and then subcategory (e.g., <i>service mesh</i> for an overview of all available CNCF service meshes).
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="table-wrapper">
|
||||
<table id="headers">
|
||||
<tr class="landscape first-line">
|
||||
<td class="sticky">
|
||||
<span> Project </span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="landscape">
|
||||
<td class="sticky">
|
||||
<span>Description</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="landscape alternate-line">
|
||||
<td class="sticky">
|
||||
<span>Maturity</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="landscape">
|
||||
<td class="sticky">
|
||||
<span>Target Users</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="landscape alternate-line">
|
||||
<td class="sticky">
|
||||
<span>Tags</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="landscape">
|
||||
<td class="sticky">
|
||||
<span>Use Case</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="landscape alternate-line">
|
||||
<td class="sticky">
|
||||
<span>Business Use</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="landscape">
|
||||
<td class="sticky">
|
||||
<span>Languages</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="landscape alternate-line">
|
||||
<td class="sticky">
|
||||
<span>First Commit</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="landscape">
|
||||
<td class="sticky">
|
||||
<span>Last Commit</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="landscape alternate-line">
|
||||
<td class="sticky">
|
||||
<span>Release Cadence</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="landscape">
|
||||
<td class="sticky">
|
||||
<span>Github Stars</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="landscape alternate-line">
|
||||
<td class="sticky">
|
||||
<span>Integrations</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="landscape">
|
||||
<td class="sticky">
|
||||
<span>Website</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="landscape alternate-line">
|
||||
<td class="sticky">
|
||||
<span>Github</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="landscape">
|
||||
<td class="sticky">
|
||||
<span>Overview Video</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table id="data">
|
||||
<tr class="landscape first-line">
|
||||
${projects.map( (project, index) => `
|
||||
<td class="project-name" data-project-index="${index}">${h(project.name)}</td>
|
||||
`).join('')}
|
||||
</tr>
|
||||
<tr class="landscape">
|
||||
${projects.map( (project) => `
|
||||
<td>${h((project.github_data || project)['description'])}</td>
|
||||
`).join('')}
|
||||
</tr>
|
||||
<tr class="landscape alternate-line">
|
||||
${projects.map( (project) => `
|
||||
<td>${h(project.relation)}</td>
|
||||
`).join('')}
|
||||
</tr>
|
||||
<tr class="landscape">
|
||||
${projects.map( (project) => `
|
||||
<td>${h((project.extra || {})['summary_personas']) || ' '}</td>
|
||||
`).join('')}
|
||||
</tr>
|
||||
<tr class="landscape alternate-line">
|
||||
${projects.map( (project) => `
|
||||
<td>${h((project.extra || {})['summary_tags'] || '').split(',').map( (tag) => `<div>- ${tag.trim()}</div>`).join('') }</td>
|
||||
`).join('')}
|
||||
</tr>
|
||||
<tr class="landscape">
|
||||
${projects.map( (project) => `
|
||||
<td>${h((project.extra || {})['summary_use_case']) || ' '}</td>
|
||||
`).join('')}
|
||||
</tr>
|
||||
<tr class="landscape alternate-line">
|
||||
${projects.map( (project) => `
|
||||
<td>${h((project.extra || {})['summary_business_use_case']) || ' '}</td>
|
||||
`).join('')}
|
||||
</tr>
|
||||
<tr class="landscape">
|
||||
${projects.map( (project) => `
|
||||
<td>${h(getLanguages(project))}</td>
|
||||
`).join('')}
|
||||
</tr>
|
||||
<tr class="landscape alternate-line">
|
||||
${projects.map( (project) => `
|
||||
<td>${h(getDate((project.github_start_commit_data || {}).start_date))}</td>
|
||||
`).join('')}
|
||||
</tr>
|
||||
<tr class="landscape">
|
||||
${projects.map( (project) => `
|
||||
<td>${h(getDate((project.github_data || {}).latest_commit_date))}</td>
|
||||
`).join('')}
|
||||
</tr>
|
||||
<tr class="landscape alternate-line">
|
||||
${projects.map( (project) => `
|
||||
<td>${h((project.extra || {})['summary_release_rate']) || ' '}</td>
|
||||
`).join('')}
|
||||
</tr>
|
||||
<tr class="landscape">
|
||||
${projects.map( (project) => `
|
||||
<td>${h(formatNumber((project.github_data || {}).stars))}</td>
|
||||
`).join('')}
|
||||
</tr>
|
||||
<tr class="landscape alternate-line">
|
||||
${projects.map( (project) => `
|
||||
<td>${highlightLinks((project.extra || {})['summary_integrations']) || ' '}</td>
|
||||
`).join('')}
|
||||
</tr>
|
||||
<tr class="landscape">
|
||||
${projects.map( (project) => `
|
||||
<td><a href="${h((project.homepage_url))}" target="_blank">${l(project.homepage_url)}</a></td>
|
||||
`).join('')}
|
||||
</tr>
|
||||
<tr class="landscape alternate-line">
|
||||
${projects.map( (project) => project.repo_url ? `
|
||||
<td><a href="${h(project.repo_url)}" target="_blank">${l(project.repo_url)}</a></td>
|
||||
`: '<td> </td>').join('')}
|
||||
</tr>
|
||||
<tr class="landscape">
|
||||
${projects.map( (project) => project.extra && project.extra.summary_intro_url ? `
|
||||
<td><a href="${h(project.extra.summary_intro_url)}" target="_blank">${l(project.extra.summary_intro_url)}</a></td>
|
||||
`: '<td> </td>').join('')}
|
||||
</tr>
|
||||
</table>
|
||||
<div style="height: 20px;"></div>
|
||||
</div>
|
||||
<script>
|
||||
function setHeight() {
|
||||
const rows = [...document.querySelectorAll('#data tr')];
|
||||
const headersRows = [...document.querySelectorAll('#headers tr')];
|
||||
for (let row of rows) {
|
||||
const index = rows.indexOf(row);
|
||||
const headerEl = headersRows[index].querySelector('td');
|
||||
const firstEl = [...row.querySelectorAll('td')].filter((x) => x.style.display !== 'none')[0];
|
||||
headerEl.style.height = (firstEl.getBoundingClientRect().height) + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
window.App = {
|
||||
totalCount: ${projects.length},
|
||||
categories: ${JSON.stringify(categories)},
|
||||
categoryItems: ${JSON.stringify(categoryItems)},
|
||||
subcategories: ${JSON.stringify(subcategories)}
|
||||
};
|
||||
document.querySelector('.categories select').addEventListener('change', function(e) {
|
||||
const selectedOption = Array.from(document.querySelectorAll('.categories option')).find( (x) => x.selected);
|
||||
const categoryId = selectedOption.value;
|
||||
if (!categoryId) {
|
||||
document.querySelector('#data').style.width = '';
|
||||
document.querySelector('.subcategories').style.display = 'none';
|
||||
} else {
|
||||
document.querySelector('.subcategories').style.display = '';
|
||||
const newWidth = ${columnWidth} * App.categoryItems[categoryId].length;
|
||||
document.querySelector('#data').style.width = newWidth + 'px';
|
||||
|
||||
const subcategories = window.App.subcategories[categoryId];
|
||||
const baseMarkup = '<option value="">All</option>';
|
||||
const markup = subcategories.map( (s) => '<option value="' + s + '">' + s + ': ' + window.App.categoryItems[categoryId + ':' + s].length + '</option>').join('');
|
||||
document.querySelector('.subcategories select').innerHTML = baseMarkup + markup;
|
||||
|
||||
}
|
||||
|
||||
for (let tr of [...document.querySelectorAll('tr')]) {
|
||||
let index = 0;
|
||||
for (let td of [...tr.querySelectorAll('td')].slice(1)) {
|
||||
const isVisible = categoryId ? App.categoryItems[categoryId].includes(index) : true;
|
||||
td.style.display = isVisible ? '' : 'none';
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
setHeight();
|
||||
});
|
||||
|
||||
document.querySelector('.subcategories select').addEventListener('change', function(e) {
|
||||
const categoryId = Array.from(document.querySelectorAll('.categories option')).find( (x) => x.selected).value;
|
||||
const subcategoryId = Array.from(document.querySelectorAll('.subcategories option')).find( (x) => x.selected).value;
|
||||
|
||||
let key = subcategoryId ? (categoryId + ':' + subcategoryId) : categoryId;
|
||||
|
||||
const newWidth = ${columnWidth} * App.categoryItems[key].length;
|
||||
document.querySelector('#data').style.width = newWidth + 'px';
|
||||
|
||||
for (let tr of [...document.querySelectorAll('tr')]) {
|
||||
let index = 0;
|
||||
for (let td of [...tr.querySelectorAll('td')].slice(1)) {
|
||||
const isVisible = App.categoryItems[key].includes(index);
|
||||
td.style.display = isVisible ? '' : 'none';
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
setHeight();
|
||||
});
|
||||
setHeight();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
`
|
||||
}
|
|
@ -1,109 +0,0 @@
|
|||
import React from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
import Select from '@material-ui/core/Select';
|
||||
import Checkbox from '@material-ui/core/Checkbox';
|
||||
import ListItemText from '@material-ui/core/ListItemText';
|
||||
import _ from 'lodash';
|
||||
|
||||
|
||||
const TreeSelector = ({value, options, onChange}) => {
|
||||
const renderValue = function(selected) {
|
||||
if (selected.length === 0) {
|
||||
return 'Any';
|
||||
}
|
||||
const properOptions = selected.filter(function(selectedId) {
|
||||
const option = _.find(options, {id: selectedId});
|
||||
if (option.level === 1) {
|
||||
return _.every(option.children, function(childId) {
|
||||
return selected.indexOf(childId) !== -1;
|
||||
});
|
||||
}
|
||||
if (option.level === 2) {
|
||||
const parentOption = _.find(options, {id: option.parentId});
|
||||
const isEverythingSelected = _.every(parentOption.children, function(childId) {
|
||||
return selected.indexOf(childId) !== -1;
|
||||
});
|
||||
return !isEverythingSelected;
|
||||
}
|
||||
});
|
||||
return properOptions.map( (x) => _.find(options, {id: x}).label).join(', ');
|
||||
}
|
||||
const onItemChanged = function(newSelection) {
|
||||
// we have new list of checked items(newSelection and previous list of
|
||||
// checked items(value), we want to get a single item which was checked /
|
||||
// unchecked
|
||||
var itemId = _.difference(newSelection, value).concat(_.difference(value, newSelection))[0];
|
||||
var newValue;
|
||||
const option = _.find(options, {id: itemId});
|
||||
const withoutCategory = function() {
|
||||
return value.filter(function(v) {
|
||||
return v !== option.id && option.children.indexOf(v) === -1;
|
||||
});
|
||||
}
|
||||
const checkedCategory = function() {
|
||||
return options.filter(function(o) {
|
||||
return o.id === option.id || option.children.indexOf(o.id) !== -1;
|
||||
}).map(function(o) { return o.id});
|
||||
}
|
||||
if (option.level === 1) {
|
||||
if (value.indexOf(option.id) !== -1) {
|
||||
//uncheck category
|
||||
newValue = withoutCategory();
|
||||
} else {
|
||||
//check category
|
||||
newValue = withoutCategory().concat(checkedCategory());
|
||||
}
|
||||
} else if (option.level === 2) {
|
||||
if (value.indexOf(option.id) !== -1) {
|
||||
//uncheck subcategory, may be whole category
|
||||
newValue = value.filter(function(x) {
|
||||
return x !== option.id;
|
||||
});
|
||||
let parentCategory = _.find(options, {id: option.parentId});
|
||||
if (_.every(parentCategory.children, function(childId) {
|
||||
return newValue.indexOf(childId) === -1;
|
||||
})) {
|
||||
newValue = newValue.filter(function(x) {
|
||||
return x !== parentCategory.id;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
//check subcategory, may be whole category
|
||||
newValue = value.concat([option.id]);
|
||||
let parentCategory = _.find(options, {id: option.parentId});
|
||||
if (_.every(parentCategory.children, function(childId) {
|
||||
return newValue.indexOf(childId) !== -1;
|
||||
})) {
|
||||
newValue = newValue.concat([parentCategory.id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
newValue = options.filter(function(o) {
|
||||
return newValue.indexOf(o.id) !== -1;
|
||||
}).map(function(o) {
|
||||
return o.id;
|
||||
});
|
||||
onChange(newValue);
|
||||
};
|
||||
|
||||
return <Select
|
||||
multiple
|
||||
style={{width:175 ,fontSize:'0.8em'}}
|
||||
value={value}
|
||||
renderValue={renderValue }
|
||||
onChange={(e) => onItemChanged(e.target.value)}
|
||||
displayEmpty
|
||||
>
|
||||
{ options.map( (el) => (
|
||||
<MenuItem key={el.id}
|
||||
value={el.id}
|
||||
style={{height:5}}>
|
||||
<span style={{width: (el.level - 1) * 20 }}/>
|
||||
<Checkbox color="primary" disableRipple checked={value.indexOf(el.id) !== -1} />
|
||||
<ListItemText disableTypography primary={el.label} style={{fontSize:'0.8em', padding:0}} />
|
||||
</MenuItem>
|
||||
)) }
|
||||
</Select>
|
||||
};
|
||||
export default pure(TreeSelector);
|
|
@ -1,46 +0,0 @@
|
|||
// locate zoom buttons
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import qs from 'query-string';
|
||||
import settings from 'public/settings.json'
|
||||
|
||||
const bird = ( <svg
|
||||
viewBox="0 0 300 244">
|
||||
<g transform="translate(-539.17946,-568.85777)" >
|
||||
<path fillOpacity="1" fillRule="nonzero"
|
||||
d="m 633.89823,812.04479 c 112.46038,0 173.95627,-93.16765 173.95627,-173.95625 0,-2.64628 -0.0539,-5.28062 -0.1726,-7.90305 11.93799,-8.63016 22.31446,-19.39999 30.49762,-31.65984 -10.95459,4.86937 -22.74358,8.14741 -35.11071,9.62551 12.62341,-7.56929 22.31446,-19.54304 26.88583,-33.81739 -11.81284,7.00307 -24.89517,12.09297 -38.82383,14.84055 -11.15723,-11.88436 -27.04079,-19.31655 -44.62892,-19.31655 -33.76374,0 -61.14426,27.38052 -61.14426,61.13233 0,4.79784 0.5364,9.46458 1.58538,13.94057 -50.81546,-2.55686 -95.87353,-26.88582 -126.02546,-63.87991 -5.25082,9.03545 -8.27852,19.53111 -8.27852,30.73006 0,21.21186 10.79366,39.93837 27.20766,50.89296 -10.03077,-0.30992 -19.45363,-3.06348 -27.69044,-7.64676 -0.009,0.25652 -0.009,0.50661 -0.009,0.78077 0,29.60957 21.07478,54.3319 49.0513,59.93435 -5.13757,1.40062 -10.54335,2.15158 -16.12196,2.15158 -3.93364,0 -7.76596,-0.38716 -11.49099,-1.1026 7.78383,24.2932 30.35457,41.97073 57.11525,42.46543 -20.92578,16.40207 -47.28712,26.17062 -75.93712,26.17062 -4.92898,0 -9.79834,-0.28036 -14.58427,-0.84634 27.05868,17.34379 59.18936,27.46396 93.72193,27.46396" />
|
||||
</g>
|
||||
</svg>);
|
||||
|
||||
const TweetButton = function({cls}) {
|
||||
const [ready, setReady] = useState(false)
|
||||
const [isHidden, setIsHidden] = useState(false)
|
||||
const tweetRef = useRef()
|
||||
|
||||
useEffect(() => {
|
||||
setReady(true)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const tweetButtonStyle = tweetRef.current && window.getComputedStyle(tweetRef.current)
|
||||
setIsHidden(tweetRef.current && (tweetButtonStyle.display === 'none' || tweetButtonStyle.visibility === 'hidden'))
|
||||
}, [ready])
|
||||
|
||||
if (!ready) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { origin, pathname } = window.location
|
||||
const url = `${origin}${pathname}`
|
||||
const { text } = settings.twitter
|
||||
const params = qs.stringify({ text, url })
|
||||
const twitterUrl = `https://twitter.com/intent/tweet?${params}`
|
||||
|
||||
return <div className={`tweet-button ${cls}`} style={{ ...(isHidden ? { display: 'none' } : null)}}>
|
||||
<a ref={tweetRef} href={twitterUrl}>{bird}<span>Tweet</span></a>
|
||||
<div className="tweet-count-wrapper">
|
||||
<div className="tweet-count">{process.env.tweets}</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
export default TweetButton
|
|
@ -1,43 +0,0 @@
|
|||
import { useRef, useEffect } from 'react'
|
||||
import { Timeline } from 'react-twitter-widgets'
|
||||
import useCurrentDevice from '../utils/useCurrentDevice'
|
||||
|
||||
const TwitterTimeline = ({ twitter }) => {
|
||||
const timelineRef = useRef(null)
|
||||
const name = twitter.split('/').pop()
|
||||
const currentDevice = useCurrentDevice()
|
||||
|
||||
// This is a hack to fix overflow issues on Safari iPhone
|
||||
// see https://github.com/cncf/landscapeapp/issues/331
|
||||
useEffect(() => {
|
||||
if (currentDevice.ios() && navigator.vendor.match(/^apple/i)) {
|
||||
timelineRef.addEventListener("DOMSubtreeModified", (el) => {
|
||||
if (el.target.tagName === "IFRAME") {
|
||||
const head = el.target.contentDocument.head;
|
||||
const newStyle = el.target.contentDocument.createElement("style");
|
||||
const css = [
|
||||
".TweetAuthor { max-width: 300px; text-overflow: ellipsis; }",
|
||||
".timeline-Tweet-text a:not(.customisable) { word-break: break-all; }"
|
||||
];
|
||||
newStyle.innerHTML = css.join(" ");
|
||||
head.appendChild(newStyle);
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
return <div ref={timelineRef}>
|
||||
<Timeline
|
||||
dataSource={{
|
||||
sourceType: 'profile',
|
||||
screenName: name
|
||||
}}
|
||||
options={{
|
||||
username: name,
|
||||
tweetLimit: 3
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
export default TwitterTimeline
|
|
@ -0,0 +1,66 @@
|
|||
const { renderItem } = require("./Item");
|
||||
const { h } = require('../utils/format');
|
||||
|
||||
const {
|
||||
calculateVerticalCategory,
|
||||
categoryTitleHeight,
|
||||
itemMargin, smallItemWidth,
|
||||
subcategoryMargin
|
||||
} = require("../utils/landscapeCalculations");
|
||||
const { renderSubcategoryInfo } = require( './SubcategoryInfo');
|
||||
const { renderCategoryHeader } = require('./CategoryHeader');
|
||||
|
||||
module.exports.renderVerticalCategory = function({header, guideInfo, subcategories, top, left, width, height, color, href, fitWidth}) {
|
||||
const subcategoriesWithCalculations = calculateVerticalCategory({ subcategories, fitWidth, width });
|
||||
return `<div>
|
||||
<div style="
|
||||
position: absolute;
|
||||
top: ${top}px;
|
||||
left: ${left}px;
|
||||
height: ${height}px;
|
||||
width: ${width}px;
|
||||
background: ${color};
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.2);
|
||||
padding: 0px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
" class="big-picture-section">
|
||||
<div style="height: ${categoryTitleHeight}px; width: 100%; display: flex;">
|
||||
${renderCategoryHeader({href: href, label: header, guideAnchor: guideInfo, background: color})}
|
||||
</div>
|
||||
<div style="
|
||||
width: 100%;
|
||||
position: relative;
|
||||
flex: 1;
|
||||
padding: ${subcategoryMargin}px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
background: white
|
||||
">
|
||||
${subcategoriesWithCalculations.map(subcategory => {
|
||||
const { guideInfo, width, columns, name } = subcategory;
|
||||
const style = `
|
||||
display: grid;
|
||||
grid-template-columns: repeat(${columns}, ${smallItemWidth}px);
|
||||
`;
|
||||
const extraStyle = fitWidth ? `justify-content: space-evenly; flex: 1;` : `grid-gap: ${itemMargin}px; `
|
||||
|
||||
return `<div style="
|
||||
position: relative;
|
||||
flex-grow: ${subcategory.rows};
|
||||
display: flex;
|
||||
flex-direction: column;">
|
||||
<div style="line-height: 15px; text-align: center;">
|
||||
<a data-type=internal href="${subcategory.href}">${h(name)}</a>
|
||||
</div>
|
||||
<div style="width: ${width}px; overflow: hidden; margin: 0 auto; ${style} ${extraStyle}">
|
||||
${subcategory.allItems.map(renderItem).join('')}
|
||||
${guideInfo ? renderSubcategoryInfo({label: name, anchor: guideInfo, column: columns}) : ''}
|
||||
</div>
|
||||
</div>`
|
||||
}).join('')}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
import { createContext } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { parseParams } from '../utils/routing'
|
||||
import { findLandscapeSettings } from '../utils/landscapeSettings'
|
||||
import { getGroupedItemsForContentMode } from '../utils/itemsCalculator'
|
||||
import selectedItemCalculator from '../utils/selectedItemCalculator'
|
||||
import { calculateSize } from '../utils/landscapeCalculations'
|
||||
import { stringifyParams } from '../utils/routing'
|
||||
|
||||
const LandscapeContext = createContext()
|
||||
|
||||
export const LandscapeProvider = ({ entries, pageParams, guideIndex = {}, children }) => {
|
||||
const router = useRouter()
|
||||
const params = parseParams({ ...pageParams, ...router.query })
|
||||
|
||||
const landscapeSettings = findLandscapeSettings(params.mainContentMode)
|
||||
const isBigPicture = params.mainContentMode !== 'card-mode'
|
||||
const groupedItems = getGroupedItemsForContentMode(params, entries, landscapeSettings)
|
||||
const selectedItemId = params.selectedItemId
|
||||
const { nextItemId, previousItemId } = selectedItemCalculator(groupedItems, selectedItemId, isBigPicture)
|
||||
const size = calculateSize(landscapeSettings)
|
||||
|
||||
const navigate = (newParams = {}, options = {}) => {
|
||||
const filters = { ...(params.filters || {}), ...(newParams.filters || {}) }
|
||||
const url = stringifyParams({ ...params, ...newParams, filters })
|
||||
router.push(url, null, options)
|
||||
}
|
||||
|
||||
const baseProps = {
|
||||
entries,
|
||||
navigate,
|
||||
groupedItems,
|
||||
nextItemId,
|
||||
previousItemId,
|
||||
params,
|
||||
landscapeSettings,
|
||||
guideIndex,
|
||||
...size
|
||||
}
|
||||
|
||||
return <LandscapeContext.Provider value={baseProps}>
|
||||
{children}
|
||||
</LandscapeContext.Provider>
|
||||
}
|
||||
|
||||
export default LandscapeContext
|
|
@ -0,0 +1,205 @@
|
|||
// An embedded version of a script
|
||||
const CncfLandscapeApp = {
|
||||
init: function() {
|
||||
setInterval(function() {
|
||||
window.parent.postMessage({
|
||||
type: 'landscapeapp-resize',
|
||||
height: document.body.scrollHeight
|
||||
}, '*');
|
||||
}, 1000);
|
||||
|
||||
this.state = {
|
||||
selected: null
|
||||
}
|
||||
|
||||
if (window.parentIFrame) {
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.keyCode === 27) {
|
||||
if (CncfLandscapeApp.state.selected) {
|
||||
this.state.selected = null;
|
||||
this.hideSelectedItem();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.body.addEventListener('click', (e) => {
|
||||
const cardEl = e.target.closest('[data-id]');
|
||||
if (cardEl) {
|
||||
const selectedItemId = cardEl.getAttribute('data-id');
|
||||
CncfLandscapeApp.state.selected = selectedItemId;
|
||||
this.showSelectedItem(selectedItemId);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
const modalBodyEl = e.target.closest('.modal-body');
|
||||
const shadowEl = e.target.closest('.modal-container');
|
||||
if (shadowEl && !modalBodyEl) {
|
||||
this.state.selected = null;
|
||||
this.hideSelectedItem();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
const modalClose = e.target.closest('.modal-close');
|
||||
if (modalClose) {
|
||||
this.state.selected = null;
|
||||
this.hideSelectedItem();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
const nextItem = e.target.closest('.modal-next');
|
||||
if (nextItem && CncfLandscapeApp.nextItemId) {
|
||||
CncfLandscapeApp.state.selected = CncfLandscapeApp.nextItemId;
|
||||
this.showSelectedItem(CncfLandscapeApp.state.selected);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
const prevItem = e.target.closest('.modal-prev');
|
||||
if (prevItem && CncfLandscapeApp.prevItemId) {
|
||||
CncfLandscapeApp.state.selected = CncfLandscapeApp.prevItemId;
|
||||
this.showSelectedItem(CncfLandscapeApp.state.selected);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
const selectedItemInternalLinkEl = e.target.closest('.modal-body a[data-type=internal]')
|
||||
if (selectedItemInternalLinkEl) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return
|
||||
}
|
||||
|
||||
}, false);
|
||||
|
||||
// support custom css styles and custom js eval code through iframe
|
||||
window.addEventListener('message', (event) => {
|
||||
var data = event.data;
|
||||
if (data.type === "css") {
|
||||
var styles = data.css;
|
||||
var el = document.createElement('style');
|
||||
el.type = 'text/css';
|
||||
if (el.styleSheet) {
|
||||
el.styleSheet.cssText = styles;
|
||||
} else {
|
||||
el.appendChild(document.createTextNode(styles));
|
||||
}
|
||||
document.getElementsByTagName("head")[0].appendChild(el);
|
||||
}
|
||||
if (data.type === "js") {
|
||||
eval(data.js);
|
||||
}
|
||||
});
|
||||
|
||||
// support css styles via param
|
||||
const params = new URLSearchParams(window.location.search.substring(1));
|
||||
if (params.get('css')) {
|
||||
const element = document.createElement("link");
|
||||
element.setAttribute("rel", "stylesheet");
|
||||
element.setAttribute("type", "text/css");
|
||||
element.setAttribute("href", params.get('css'));
|
||||
document.getElementsByTagName("head")[0].appendChild(element);
|
||||
}
|
||||
if (params.get('style')) {
|
||||
const element = document.createElement("style");
|
||||
let style = params.get('style');
|
||||
try {
|
||||
style = JSON.parse(style)
|
||||
} catch(ex) {
|
||||
|
||||
}
|
||||
element.innerHTML = style;
|
||||
document.getElementsByTagName("head")[0].appendChild(element);
|
||||
}
|
||||
},
|
||||
|
||||
showSelectedItem: async function(selectedItemId) {
|
||||
if (!window.parentIFrame) {
|
||||
window.parent.postMessage({
|
||||
type: 'landscapeapp-show',
|
||||
selected: selectedItemId,
|
||||
location: {
|
||||
search: window.location.search,
|
||||
pathname: window.location.pathname
|
||||
}
|
||||
}, '*');
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectedItems = this.selectedItems || {};
|
||||
if (!this.selectedItems[selectedItemId]) {
|
||||
const result = await fetch(`${this.basePath}/data/items/info-${selectedItemId}.html`);
|
||||
const text = await result.text();
|
||||
this.selectedItems[selectedItemId] = text;
|
||||
}
|
||||
document.querySelector('.modal').style.display="";
|
||||
document.querySelector('.modal .modal-content').outerHTML = this.selectedItems[selectedItemId];
|
||||
document.querySelector('body').style.overflow = 'hidden';
|
||||
|
||||
if (window.twttr) {
|
||||
window.twttr.widgets.load();
|
||||
} else {
|
||||
setTimeout( () => window.twttr && window.twttr.widgets.load(), 1000);
|
||||
}
|
||||
|
||||
//calculate previous and next items;
|
||||
const selectedItemEl = document.querySelector(`[data-id=${selectedItemId}]`);
|
||||
const parent = selectedItemEl.closest('.cards-section');
|
||||
const allItems = parent.querySelectorAll('[data-id]');
|
||||
const index = [].indexOf.call(allItems, selectedItemEl);
|
||||
const prevItem = index > 0 ? allItems[index - 1].getAttribute('data-id') : null;
|
||||
const nextItem = index < allItems.length - 1 ? allItems[index + 1].getAttribute('data-id') : null;
|
||||
|
||||
this.nextItemId = nextItem;
|
||||
this.prevItemId = prevItem;
|
||||
|
||||
if (nextItem) {
|
||||
document.querySelector('.modal-next').removeAttribute('disabled');
|
||||
} else {
|
||||
document.querySelector('.modal-next').setAttribute('disabled', '');
|
||||
}
|
||||
|
||||
if (prevItem) {
|
||||
document.querySelector('.modal-prev').removeAttribute('disabled');
|
||||
} else {
|
||||
document.querySelector('.modal-prev').setAttribute('disabled', '');
|
||||
}
|
||||
|
||||
if (window.parentIFrame) {
|
||||
window.parentIFrame.sendMessage({type: 'showModal'});
|
||||
window.parentIFrame.getPageInfo(function(info) {
|
||||
var offset = info.scrollTop - info.offsetTop;
|
||||
var maxHeight = info.clientHeight * 0.9;
|
||||
if (maxHeight > 640) {
|
||||
maxHeight = 640;
|
||||
}
|
||||
var defaultTop = (info.windowHeight - maxHeight) / 2;
|
||||
var top = defaultTop + offset;
|
||||
if (top < 0 && info.iframeHeight <= 600) {
|
||||
top = 10;
|
||||
}
|
||||
setTimeout(function() {
|
||||
const modal = document.querySelector('.modal-body');
|
||||
if (modal) {
|
||||
modal.style.top = top + 'px';
|
||||
modal.style.marginTop = 0;
|
||||
modal.style.marginBottom = 0;
|
||||
modal.style.bottom = '';
|
||||
modal.style.maxHeight = maxHeight + 'px';
|
||||
}
|
||||
}, 10);
|
||||
});
|
||||
}
|
||||
},
|
||||
hideSelectedItem: function() {
|
||||
document.querySelector('.modal').style.display="none";
|
||||
document.querySelector('body').style.overflow = '';
|
||||
if (window.parentIFrame) {
|
||||
window.parentIFrame.sendMessage({type: 'hideModal'})
|
||||
}
|
||||
}
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', () => CncfLandscapeApp.init());
|
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue