├── .gitignore
├── client
├── package.json
├── src
│ ├── css
│ │ ├── colors.css
│ │ ├── colors.vars.css
│ │ ├── form.css
│ │ ├── icons.vars.css
│ │ ├── index.css
│ │ ├── layout.css
│ │ ├── layout.vars.css
│ │ ├── loading.css
│ │ ├── reset.css
│ │ ├── scroll.css
│ │ ├── sidebar.css
│ │ ├── style.css
│ │ ├── tag.css
│ │ └── text.css
│ ├── font
│ │ ├── cutive_mono.woff2
│ │ ├── muli_300.woff2
│ │ └── open_sans_400.woff2
│ ├── img
│ │ └── logo.png
│ ├── index.ejs
│ └── js
│ │ ├── components
│ │ ├── mixins
│ │ │ ├── collapsable-adder.jsx
│ │ │ ├── collapse.jsx
│ │ │ ├── flip-list.jsx
│ │ │ ├── link.jsx
│ │ │ ├── render-canceler.jsx
│ │ │ ├── render-delayer.jsx
│ │ │ └── reply-layout
│ │ │ │ ├── index.js
│ │ │ │ ├── reply-layout.css
│ │ │ │ └── reply-layout.jsx
│ │ ├── pages
│ │ │ ├── about.jsx
│ │ │ ├── auth.jsx
│ │ │ ├── landing
│ │ │ │ ├── index.js
│ │ │ │ ├── landing.css
│ │ │ │ └── landing.jsx
│ │ │ ├── needs-and-libs
│ │ │ │ ├── index.js
│ │ │ │ ├── needs-and-libs.css
│ │ │ │ └── needs-and-libs.jsx
│ │ │ ├── not-found-404.jsx
│ │ │ ├── redirect-catch.jsx
│ │ │ ├── tag-resource.jsx
│ │ │ └── tag.jsx
│ │ └── snippets
│ │ │ ├── add-resource.jsx
│ │ │ ├── button
│ │ │ ├── button.css
│ │ │ ├── button.jsx
│ │ │ └── index.js
│ │ │ ├── catalog-title.jsx
│ │ │ ├── category
│ │ │ ├── category.css
│ │ │ ├── category.jsx
│ │ │ └── index.js
│ │ │ ├── comment-list
│ │ │ ├── comment-list.css
│ │ │ ├── comment-list.jsx
│ │ │ ├── comment.jsx
│ │ │ └── index.js
│ │ │ ├── description-line
│ │ │ ├── description-line.css
│ │ │ ├── description-line.jsx
│ │ │ └── index.js
│ │ │ ├── eraser.jsx
│ │ │ ├── icon
│ │ │ ├── icon.css
│ │ │ ├── icon.jsx
│ │ │ └── index.js
│ │ │ ├── loading
│ │ │ ├── css-loader
│ │ │ │ ├── css-loader.css
│ │ │ │ ├── css-loader.jsx
│ │ │ │ └── index.js
│ │ │ ├── index.js
│ │ │ ├── loading.jsx
│ │ │ └── react-loader
│ │ │ │ ├── index.js
│ │ │ │ ├── react-loader.css
│ │ │ │ └── react-loader.jsx
│ │ │ ├── login-required.jsx
│ │ │ ├── logo-devarchy
│ │ │ ├── index.js
│ │ │ ├── logo-devarchy.css
│ │ │ └── logo-devarchy.jsx
│ │ │ ├── logo-section
│ │ │ ├── index.js
│ │ │ ├── logo-section.css
│ │ │ └── logo-section.jsx
│ │ │ ├── markdown-list-view
│ │ │ ├── index.js
│ │ │ ├── markdown-list-view.css
│ │ │ └── markdown-list-view.jsx
│ │ │ ├── month-picker
│ │ │ ├── index.js
│ │ │ ├── month-picker.css
│ │ │ └── month-picker.jsx
│ │ │ ├── popular-board.jsx
│ │ │ ├── progressbar
│ │ │ ├── index.js
│ │ │ ├── progressbar.css
│ │ │ └── progressbar.js
│ │ │ ├── resource-details
│ │ │ ├── index.js
│ │ │ ├── resource-details.css
│ │ │ └── resource-details.jsx
│ │ │ ├── resource-line
│ │ │ ├── index.js
│ │ │ ├── resource-line.css
│ │ │ └── resource-line.jsx
│ │ │ ├── resource-list.jsx
│ │ │ ├── resource-request-list.jsx
│ │ │ ├── resource-view
│ │ │ ├── index.js
│ │ │ ├── resource-view.css
│ │ │ └── resource-view.jsx
│ │ │ ├── resources-graveyard-crib.jsx
│ │ │ ├── reviewpoint-list
│ │ │ ├── index.js
│ │ │ ├── review-point.jsx
│ │ │ ├── reviewpoint-list.css
│ │ │ └── reviewpoint-list.jsx
│ │ │ ├── social-buttons
│ │ │ ├── index.js
│ │ │ ├── social-buttons.css
│ │ │ └── social-buttons.jsx
│ │ │ ├── tag-list
│ │ │ ├── index.js
│ │ │ ├── tag-list.css
│ │ │ └── tag-list.jsx
│ │ │ ├── tag-resources-view.jsx
│ │ │ ├── topic
│ │ │ ├── index.js
│ │ │ ├── topic.css
│ │ │ └── topic.jsx
│ │ │ ├── user.jsx
│ │ │ ├── userinfo.jsx
│ │ │ └── vote-block
│ │ │ ├── index.js
│ │ │ ├── vote-block.css
│ │ │ └── vote-block.jsx
│ │ ├── index.js
│ │ ├── navigation.js
│ │ ├── page.js
│ │ ├── rerender.js
│ │ ├── router.js
│ │ ├── thing
│ │ ├── comment.js
│ │ ├── genericvote.js
│ │ ├── http.js
│ │ ├── index.js
│ │ ├── mixins
│ │ │ ├── commentable.js
│ │ │ └── votable.js
│ │ ├── resource.js
│ │ ├── reviewpoint.js
│ │ ├── tag.js
│ │ ├── thing.js
│ │ └── user.js
│ │ ├── user_tracker.js
│ │ └── util
│ │ ├── debouncer.js
│ │ ├── http.js
│ │ ├── normalize_url.js
│ │ ├── npm_package_name_validation.js
│ │ ├── pretty_print.js
│ │ ├── route_spec.js
│ │ ├── server_uri.js
│ │ └── text_search.js
├── webpack.config.js
└── yarn.lock
├── dev.sh
├── license.md
├── production.sh
├── readme.md
└── server
├── .logs
└── placeholder
├── database
├── connection.js
├── index.js
├── interactive.js
├── migrations.js
├── schema.js
└── thingdb
│ ├── db_interface
│ ├── create_transaction.js
│ ├── index.js
│ ├── load
│ │ ├── events.js
│ │ ├── index.js
│ │ ├── things.js
│ │ ├── util.js
│ │ └── view.js
│ ├── save.js
│ └── table
│ │ └── index.js
│ ├── debug
│ ├── get_things_sync.js
│ ├── index.js
│ └── log.js
│ ├── index.js
│ ├── interpolate
│ ├── aggregate_events.js
│ ├── apply_defaults.js
│ ├── apply_side_effects.js
│ ├── compute_values.js
│ ├── compute_views.js
│ └── index.js
│ ├── migrate.js
│ ├── plugins
│ ├── memory-cache
│ │ └── index.js
│ └── serializer
│ │ └── index.js
│ ├── schema_common.js
│ ├── test
│ ├── devarchy
│ │ ├── index.js
│ │ ├── playground.js
│ │ ├── population.js
│ │ ├── setup.js
│ │ ├── tests
│ │ │ ├── frontend.js
│ │ │ ├── markdown_translation.js
│ │ │ └── schema.js
│ │ └── thing.js
│ ├── features
│ │ ├── index.js
│ │ ├── population.js
│ │ ├── schema.js
│ │ ├── setup.js
│ │ ├── tests
│ │ │ ├── cascading_save.js
│ │ │ ├── computed_props.js
│ │ │ ├── concurrency.js
│ │ │ ├── core.js
│ │ │ ├── details.js
│ │ │ ├── loading.js
│ │ │ ├── migrations.js
│ │ │ ├── misc.js
│ │ │ ├── relations.js
│ │ │ ├── subtypes.js
│ │ │ ├── uniqueness.js
│ │ │ ├── upsert.js
│ │ │ ├── validation.js
│ │ │ └── views.js
│ │ └── thing.js
│ ├── index.js
│ ├── mocha.opts
│ ├── test-promise.js
│ └── thing.js
│ ├── util
│ ├── deep-assign.js
│ ├── deep-equal.js
│ └── obj-path.js
│ └── validate
│ ├── index.js
│ ├── load_props.js
│ ├── schema.js
│ └── thing.js
├── http
├── api.js
├── auth
│ ├── index.js
│ └── providers.js
├── client
│ ├── html.js
│ └── index.js
├── config.js
├── error_handler.js
├── http_cache.js
├── index.js
├── log.js
└── static_dir.js
├── index.js
├── package.json
├── process.pm2.json
├── server_down.js
├── test
├── index.js
└── mocha.opts
├── util
├── b64_unicode.js
├── deepFreeze.js
├── ensure_database_connection.js
├── env.js
├── error_tracker.js
├── fetch.js
├── fetch_with_cache.js
├── github-api.js
├── gitlab-api.js
├── html-api.js
├── long_term_cache.js
├── memoize.js
├── multiline_tag.js
├── nodejs_hash.js
├── normalize_url.js
├── npm-api.js
├── npm_package_name_validation.js
├── parse_markdown_catalog
│ ├── categorize.js
│ ├── index.js
│ ├── linear_processing
│ │ ├── apply_processors.js
│ │ └── processors
│ │ │ ├── github_entry.js
│ │ │ ├── index.js
│ │ │ ├── npm_entry.js
│ │ │ └── web_entry.js
│ ├── parse_markdown.js
│ ├── tests
│ │ └── test_description.md
│ └── util
│ │ ├── is_github_url.js
│ │ ├── log.js
│ │ └── options.js
└── turn_into_error_object.js
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | dist/
3 | npm-debug.log*
4 | yarn-error.log
5 | /.env
6 | /server/bot
7 | /server/.logs/*
8 | !/server/.logs/placeholder
9 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "assertion-soft": "*",
4 | "babel-polyfill": "*",
5 | "bluebird": "*",
6 | "classnames": "*",
7 | "clean-sentence": "*",
8 | "crossroads": "0.12.2",
9 | "extendable-error-class": "*",
10 | "github-markdown-css": "*",
11 | "history": "2.x.x",
12 | "isomorphic-fetch": "*",
13 | "nprogress": "*",
14 | "octicons": "4.x.x",
15 | "postcss-easing-gradients": "*",
16 | "react": "*",
17 | "react-click-outside": "tj/react-click-outside",
18 | "react-collapse": "2.3.3",
19 | "react-dom": "*",
20 | "react-flip-move": "*",
21 | "react-height": "*",
22 | "react-icons": "*",
23 | "react-md-spinner": "0.1.0",
24 | "react-month-picker": "*",
25 | "react-motion": "*",
26 | "react-scroll": "*",
27 | "smoothscroll-polyfill": "^0.3.4",
28 | "timerlog": "*",
29 | "validator": "*"
30 | },
31 | "devDependencies": {
32 | "babel-core": "*",
33 | "babel-loader": "*",
34 | "babel-preset-es2015": "*",
35 | "babel-preset-es2016": "*",
36 | "babel-preset-es2017": "*",
37 | "babel-preset-node6": "*",
38 | "babel-preset-react": "*",
39 | "babel-preset-stage-2": "*",
40 | "css-loader": "*",
41 | "cssnano": "*",
42 | "extract-text-webpack-plugin": "*",
43 | "favicons-webpack-plugin": "*",
44 | "file-loader": "*",
45 | "html-webpack-plugin": "*",
46 | "postcss-cssnext": "*",
47 | "postcss-import": "*",
48 | "postcss-loader": "*",
49 | "postcss-reporter": "*",
50 | "postcss-url": "*",
51 | "progress-bar-webpack-plugin": "*",
52 | "raw-loader": "*",
53 | "react-addons-perf": "*",
54 | "style-loader": "*",
55 | "url-loader": "*",
56 | "webpack": "*",
57 | "webpack-dev-server": "*"
58 | },
59 | "babel": {
60 | "presets": [
61 | [
62 | "es2015",
63 | {
64 | "modules": false
65 | }
66 | ],
67 | "stage-2",
68 | "es2016",
69 | "es2017",
70 | "react"
71 | ],
72 | "plugins": []
73 | },
74 | "scripts": {
75 | "start": "npm run webpack_start",
76 | "build": "npm run webpack_build",
77 | "webpack_start": "./node_modules/webpack-dev-server/bin/webpack-dev-server.js",
78 | "webpack_build": "export NODE_ENV=production && ./node_modules/webpack/bin/webpack.js"
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/client/src/css/colors.css:
--------------------------------------------------------------------------------
1 | @import './colors.vars.css';
2 |
3 | .css_color_red {
4 | color: var(--css_color_red);
5 | }
6 | .css_color_green {
7 | color: var(--css_color_green);
8 | }
9 |
10 | .css_color_error {
11 | color: black;
12 | }
13 |
--------------------------------------------------------------------------------
/client/src/css/colors.vars.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --css_color_gray: #f7f7f7;
3 |
4 | --css_color_red:
5 | /*
6 | #bd362f
7 | #a90a3b
8 | #ab0000
9 | */
10 | #d82b50
11 | ;
12 |
13 | /*
14 | --css_color_green: #009a46;
15 | --css_color_green: #009000;
16 | */
17 | --css_color_green: #14a94b;
18 |
19 | --css_color_sidebar_background: #fcfcfc;
20 | --css_color_sidebar_border: #f5f5f5;
21 |
22 | --css_color_background: #fefefe;
23 | }
24 |
--------------------------------------------------------------------------------
/client/src/css/form.css:
--------------------------------------------------------------------------------
1 | .css_description_label {
2 | color: #999;
3 | font-size: 0.9rem;
4 | margin-top: 13px;
5 | margin-bottom: 4px;
6 | }
7 |
8 | label.css_da {
9 | cursor: pointer;
10 | @nest [disabled] & {
11 | cursor: default;
12 | }
13 | }
14 |
15 | textarea.css_da,
16 | input.css_da {
17 | outline: 0;
18 | }
19 |
20 | fieldset.css_da {
21 | padding: 0;
22 | margin: 0;
23 | border: 0;
24 | }
25 |
26 | .css_input_wrapper {
27 | background-color: transparent;
28 | border: 1px solid rgba(0,0,0,0.08);
29 | cursor: text;
30 | @nest [disabled] & {
31 | cursor: default;
32 | }
33 | padding: 7px 10px;
34 | display: inline-flex;
35 | border-radius: 3px;
36 |
37 | & > input,
38 | & .sel_input_area {
39 | width: 10px;
40 | flex: 1;
41 | & input {
42 | width: 100%;
43 | }
44 | }
45 |
46 | &,
47 | & input {
48 | font-size: 14px;
49 | }
50 |
51 | & input {
52 | border: 0;
53 | outline: 0;
54 | background: transparent;
55 | padding: 0;
56 | font-family: inherit;
57 | }
58 |
59 | & > .css_input_wrapper__prefix {
60 | color: #4f4f4f;
61 | }
62 |
63 | & .sel_input_area {
64 | position: relative;
65 |
66 | & > .sel_input_placeholder {
67 | position: absolute;
68 | height: 100%;
69 | width: 100%;
70 | top: 0;
71 | left: 0;
72 | color: #ccc;
73 | pointer-events: none;
74 | &::before {
75 | content: "Search";
76 | display: block;
77 | }
78 | opacity: 0;
79 | /*
80 | transition: opacity .2s;
81 | */
82 | &.sel_input_is_empty {
83 | opacity: 1;
84 | }
85 | }
86 | }
87 | }
88 |
89 | input[type="text"].css_da,
90 | input:not([type]).css_da
91 | {
92 | vertical-align: middle;
93 | padding: 1px 3px;
94 | background-color: white;
95 | border-radius: 3px;
96 | border-width: 1px;
97 | border-style: solid;
98 | border-color: #ccc;
99 | }
100 |
101 | .css_inline_input {
102 | & > input {
103 | border: 0;
104 | padding: 0 3px;
105 | display: inline;
106 | background-color: transparent;
107 | font-size: inherit;
108 | font-family: inherit;
109 |
110 | }
111 | border-radius: 3px;
112 | border-width: 1px;
113 | border-style: solid;
114 | border-color: #ccc;
115 | }
116 |
117 | input::placeholder {
118 | opacity: 1; /* for firefox */
119 | color: #bbb;
120 | }
121 |
122 | input[type="checkbox"].css_da {
123 | vertical-align: middle;
124 | margin: 0;
125 | margin-right: 4px;
126 | display: inline-block;
127 | margin-top: -2px;
128 | }
129 |
--------------------------------------------------------------------------------
/client/src/css/index.css:
--------------------------------------------------------------------------------
1 | @import './layout.css';
2 | @import './scroll.css';
3 | @import './sidebar.css';
4 | @import './reset.css';
5 | @import './colors.css';
6 | @import './text.css';
7 | @import './style.css';
8 | @import './loading.css';
9 | @import './form.css';
10 | @import './tag.css';
11 |
--------------------------------------------------------------------------------
/client/src/css/layout.css:
--------------------------------------------------------------------------------
1 | @import './layout.vars.css';
2 |
3 | :root {
4 | --css_padding_left: 15px;
5 | --css_padding_right: 10px;
6 | }
7 |
8 | html.css_da {
9 | height: 100%;
10 | width: 100%;
11 | & > body {
12 | margin: 0;
13 | display: flex;
14 | min-width: 100%;
15 | min-height: 100%;
16 | }
17 | }
18 |
19 | .css_header {
20 | height: var(--css_header_height);
21 | display: flex;
22 | align-items: center;
23 | }
24 |
25 | .css_sidebar,
26 | .css_sidebar_wrapper {
27 | height: 100%;
28 | }
29 | .css_sidebar {
30 | overflow-y: scroll;
31 | width: var(--css_sidebar_width);
32 | }
33 |
34 | .css_tag_list {
35 | position: fixed;
36 | top: 0;
37 | left: 0;
38 | z-index: 1;
39 | height: 100%;
40 | width: calc(2px + var(--css_sidebar_width));
41 | }
42 |
43 |
44 |
45 | body,
46 | .sel_main_view {
47 | width: 100%;
48 | height: 100%;
49 | }
50 | .sel_main_view_wrapper {
51 | overflow-x: hidden;
52 | }
53 | .sel_main_view_content {
54 | transition: padding-left var(--css_layout_transition);
55 | box-sizing: content-box !important;
56 | max-width: 1200px;
57 | margin: auto;
58 | & > * {
59 | padding-left: var(--css_padding_left);
60 | padding-right: var(--css_padding_right);
61 | }
62 | }
63 | .sel_main_view_content {
64 | padding-left: var(--css_sidebar_width);
65 | width: calc(100% - var(--css_sidebar_width));
66 |
67 | @nest
68 | .css_hide_sidebar & {
69 | padding-left: 0;
70 | width: 100%;
71 | }
72 | }
73 |
74 | .css_show_resource_view {
75 | & .sel_main_view_content {
76 | padding-left: calc(var(--sel_resource_view_width));
77 | }
78 | & .sel_needs_header_prefix {
79 | width: calc(0.5 * var(--sel_resource_view_width)) !important;
80 | margin-left: calc(-0.5 * var(--sel_resource_view_width)) !important;
81 | }
82 | & .sel_main_view__responsive_content {
83 | width: calc(100vw - var(--sel_resource_view_width) - var(--css_padding_left) - var(--css_padding_right));
84 | max-width: 100%;
85 | }
86 | }
87 | .sel_main_view__responsive_content {
88 | width: 100%;
89 | transition: width var(--css_layout_transition);
90 | }
91 | .sel_needs_header_prefix {
92 | transition: width var(--css_layout_transition), margin-left var(--css_layout_transition);
93 | }
94 |
95 | .css_sidebar_padding {
96 | padding-left: var(--css_sidebar_width);
97 | }
98 |
99 | .css_center {
100 | width: 100%;
101 | height: 100%;
102 | display: flex;
103 | align-items: center;
104 | justify-content: center;
105 | }
106 |
107 |
108 |
109 | .sel_resource_view {
110 | position: fixed;
111 | top: 0;
112 | width: var(--sel_resource_view_width);
113 | left: calc( -1 * var(--sel_resource_view_width));
114 | height: 100%;
115 | z-index: 3;
116 | transition: left var(--css_layout_transition), visibility var(--css_layout_transition);
117 | visibility: hidden;
118 | }
119 | .css_show_resource_view .sel_resource_view {
120 | left: 0;
121 | visibility: visible;
122 | }
123 |
--------------------------------------------------------------------------------
/client/src/css/layout.vars.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --css_sidebar_width: 240px;
3 | --css_header_height: 116px;
4 | --sel_resource_view_width: 600px;
5 | --css_layout_transition: 0.7s;
6 | }
7 |
--------------------------------------------------------------------------------
/client/src/css/loading.css:
--------------------------------------------------------------------------------
1 | /* copy of Font Awesome's fa-spin */
2 | @-webkit-keyframes css_anim_spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes css_anim_spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}
3 | .css_spin {
4 | -webkit-animation: css_anim_spin 2s infinite linear;
5 | animation: css_anim_spin 2s infinite linear;
6 | }
7 |
--------------------------------------------------------------------------------
/client/src/css/reset.css:
--------------------------------------------------------------------------------
1 | html.css_da {
2 | & * {
3 | box-sizing: border-box;
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/client/src/css/scroll.css:
--------------------------------------------------------------------------------
1 | /* prevent scroll to propagate */
2 | * {
3 | /* https://github.com/w3c/csswg-drafts/issues/769 */
4 | /* https://bugs.chromium.org/p/chromium/issues/detail?id=672921 */
5 | scroll-boundary-behavior: contain;
6 | }
7 | /* doesn't seem to be supported in Chrome
8 | - http://stackoverflow.com/questions/25820750/what-is-css-scroll-behavior-property
9 | - https://developer.mozilla.org/en/docs/Web/CSS/scroll-behavior
10 | * {
11 | scroll-behavior: smooth;
12 | }
13 | */
14 |
15 | html.css_prevent_scroll_propagation {
16 | & .sel_main_view {
17 | overflow-y: scroll;
18 | }
19 | }
20 | html:not(.css_prevent_scroll_propagation) {
21 | & body {
22 | overflow-y: scroll;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/client/src/css/sidebar.css:
--------------------------------------------------------------------------------
1 | @import './colors.vars.css';
2 | @import './layout.vars.css';
3 |
4 | .css_sidebar {
5 | position: relative;
6 | z-index: 2;
7 | color: #3a3a3a;
8 |
9 | & .css_tag_list_content {
10 | margin: 0 0 2px 5px;
11 | }
12 |
13 | & .css_sidebar_content {
14 | position: relative;
15 | min-height: 100%;
16 | padding-bottom: 8px;
17 | }
18 | }
19 |
20 | /* we have to use a wrapper because of custom scrollbar animation trick */
21 | .css_sidebar_wrapper {
22 | background-color: var(--css_color_sidebar_background);
23 | }
24 |
25 | .css_tag_list {
26 | transition: left var(--css_layout_transition), visibility var(--css_layout_transition);
27 | left: 0;
28 | visibility: visible;
29 | @nest
30 | .css_hide_sidebar &,
31 | .css_show_resource_view &
32 | {
33 | left: calc(-1 * var(--css_sidebar_width));
34 | visibility: hidden;
35 | }
36 | }
37 |
38 | /* custom scrollbar */
39 | @media screen and (-webkit-min-device-pixel-ratio:0) { /* trick to only apply in webkit */
40 | .sel_resource_view__scroll_area,
41 | .css_sidebar {
42 | &::-webkit-scrollbar {
43 | width: 3px;
44 | }
45 | &::-webkit-scrollbar-track {
46 | background-color: var(--css_color_sidebar_border);
47 | }
48 |
49 | &::-webkit-scrollbar-thumb {
50 | background-color: rgba(0,0,0,0.1);
51 | }
52 | /* transition on hover
53 | &::-webkit-scrollbar-thumb {
54 | background-color: inherit;
55 | }
56 | transition: background-color var(--css_layout_transition);
57 | background-color: rgba(0,0,0,0);
58 | -webkit-background-clip: text;
59 | &:hover {
60 | background-color: rgba(0,0,0,0.1);
61 | }
62 | */
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/client/src/css/style.css:
--------------------------------------------------------------------------------
1 | @import './colors.vars.css';
2 | @import './layout.vars.css';
3 |
4 | html.css_da {
5 | background-color: var(--css_color_background);
6 | }
7 |
8 | html.is_loading_initial_data {
9 | & .sel_category_resources,
10 | & .sel_collapsable_adder,
11 | & .sel_needs_search_box,
12 | & .css_new_requests {
13 | cursor: wait;
14 | & * {
15 | pointer-events: none;
16 | }
17 | }
18 | }
19 |
20 | .sel_main_view_wrapper {
21 | &:after {
22 | content: "";
23 | display: block;
24 | position: absolute;
25 | top: 0;
26 | right: 0;
27 | width: 70px;
28 | height: 100%;
29 | pointer-events: none;
30 | background: scrim-gradient(
31 | to left,
32 | var(--css_color_background),
33 | transparent
34 | );
35 | opacity: 0;
36 | transition: opacity var(--css_layout_transition);
37 | @nest
38 | .css_show_resource_view & {
39 | opacity: 1;
40 | }
41 | }
42 | /* can't make .sel_main_view_wrapper positioned while using react-scroll */
43 | @nest
44 | .css_show_resource_view & {
45 | position: relative;
46 | }
47 | }
48 |
49 | .sel_highlight {
50 | background-color: yellow;
51 | color: black;
52 | font-weight: normal;
53 | }
54 |
--------------------------------------------------------------------------------
/client/src/css/tag.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --icon_width: 23px;
3 | }
4 |
5 | .css_tag {
6 | display: block;
7 | position: relative;
8 | margin: 10px 0;
9 | padding-left: calc(var(--icon_width) + 17px);
10 | color: #4b4b4b !important;
11 | font-size: 1.41rem;
12 |
13 | & .css_tag_icon {
14 | position: absolute;
15 | width: var(--icon_width);
16 | height: 100%;
17 | background-size: contain;
18 | background-repeat: no-repeat;
19 | background-position: 50%;
20 | margin-left: calc(var(--icon_width) * -1 - 4px);
21 | display: block;
22 | /* don't know why autoprefixer doesn't add -webkit- prefix */
23 | -webkit-filter: grayscale(100%);
24 | filter: grayscale(100%);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/client/src/css/text.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'Muli';
3 | font-style: normal;
4 | font-weight: 400;
5 | src: url(../font/muli_300.woff2) format('woff2');
6 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
7 | }
8 | @font-face {
9 | font-family: 'Open Sans';
10 | font-style: normal;
11 | font-weight: 400;
12 | src: url(../font/open_sans_400.woff2) format('woff2');
13 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
14 | }
15 | @font-face {
16 | font-family: 'Cutive Mono';
17 | font-style: normal;
18 | font-weight: 400;
19 | src: url(../font/cutive_mono.woff2) format('woff2');
20 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
21 | }
22 | /* tell cssnano that we need that font */
23 | .i_should_not_exist {
24 | font-family: 'Cutive Mono';
25 | }
26 |
27 | html.css_da > body,
28 | textarea.css_da,
29 | input.css_da {
30 | font-family: 'Open Sans', Verdana, Geneva, sans-serif;
31 | }
32 | .css_catalog_title {
33 | font-family: 'Muli';
34 | }
35 |
36 | html.css_da {
37 | color: #333;
38 | font-size: 0.82em;
39 | }
40 | .css_da {
41 | @nest h1&, h2&, h3&, h4&, h5& {
42 | font-weight: normal;
43 | }
44 | }
45 | h1.css_da {
46 | font-size: 1.7em;
47 | }
48 | h2.css_da {
49 | font-size: 1.55em;
50 | }
51 | h6.css_da {
52 | opacity: 0.6;
53 | font-size: .85rem;
54 | font-weight: 500;
55 | margin: 0;
56 | }
57 | .css_catalog_title {
58 | font-size: 2.4em;
59 | }
60 |
61 | a.css_a_gray {
62 | &, &:hover, &:focus {
63 | color: #aaa;
64 | cursor: pointer;
65 | text-decoration: none;
66 | }
67 | }
68 | a.css_da {
69 | &, &:hover, &:focus {
70 | color: inherit;
71 | text-decoration: none;
72 | }
73 | }
74 |
75 | code.css_da {
76 | border-radius: 3px;
77 | &:not(.css_inline) {
78 | font-size: 12px;
79 | padding: 0.2em;
80 | /*
81 | background-color: rgba(0,0,0,0.04);
82 | */
83 | }
84 | &.css_inline {
85 | padding: 0.1em 0.45em;
86 | padding-top: 2px;
87 | font-size: 1.09em;
88 | /*
89 | background-color: rgba(0,0,0,0.06);
90 | */
91 | }
92 | background-color: #f7f7f7;
93 | }
94 |
95 | .css_p {
96 | display: block !important;
97 | margin: 1em 0;
98 | }
99 |
100 | .css_note {
101 | font-size: 0.9em;
102 | color: #575757;
103 | }
104 |
105 | .css_light_paragraph {
106 | margin: 0;
107 | padding: 3px 0;
108 | font-size: 1.1em;
109 | }
110 |
111 | .css_description_line {
112 | display: block;
113 | position: relative;
114 | top: -1px;
115 | color: #999;
116 | font-size: 10.6667px;
117 | }
118 |
119 | .css_1px_up {
120 | position: relative;
121 | top: -1px;
122 | }
123 | .css_1px_down {
124 | position: relative;
125 | top: 1px;
126 | }
127 | .css_1px_left {
128 | position: relative;
129 | left: -1px;
130 | }
131 | .css_1px_right {
132 | position: relative;
133 | left: 1px;
134 | }
135 | .css_2px_up {
136 | position: relative;
137 | top: -2px;
138 | }
139 | .css_2px_down {
140 | position: relative;
141 | top: 2px;
142 | }
143 | .css_2px_left {
144 | position: relative;
145 | left: -2px;
146 | }
147 | .css_2px_right {
148 | position: relative;
149 | left: 2px;
150 | }
151 |
--------------------------------------------------------------------------------
/client/src/font/cutive_mono.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devarchy/website/31a9fcc23c8bf2c8f64a153b6959e4f38b0c37c9/client/src/font/cutive_mono.woff2
--------------------------------------------------------------------------------
/client/src/font/muli_300.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devarchy/website/31a9fcc23c8bf2c8f64a153b6959e4f38b0c37c9/client/src/font/muli_300.woff2
--------------------------------------------------------------------------------
/client/src/font/open_sans_400.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devarchy/website/31a9fcc23c8bf2c8f64a153b6959e4f38b0c37c9/client/src/font/open_sans_400.woff2
--------------------------------------------------------------------------------
/client/src/img/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devarchy/website/31a9fcc23c8bf2c8f64a153b6959e4f38b0c37c9/client/src/img/logo.png
--------------------------------------------------------------------------------
/client/src/index.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
10 |
11 |
14 | <% for (var css in htmlWebpackPlugin.files.css) { %>
15 |
16 | <% } %>
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
33 |
38 |
39 |
43 |
44 |
49 |
72 |
73 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/client/src/js/components/mixins/collapsable-adder.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import assert_soft from 'assertion-soft';
3 | import classNames from 'classnames';
4 |
5 | import {IconAdd, IconAdd2} from '../snippets/icon';
6 | import CollapseMixin from '../mixins/collapse';
7 | import user_tracker from '../../user_tracker';
8 |
9 |
10 | class CollapsableAdder extends React.Component {
11 | constructor(props) {
12 | assert_soft(props.body_content);
13 | assert_soft(props.header_text);
14 | super();
15 | this.state = {expanded: false};
16 | }
17 | toggle() {
18 | const expanded = ! this.state.expanded;
19 |
20 | this.setState({expanded});
21 |
22 | if( this.props.on_toggle ) {
23 | this.props.on_toggle(expanded);
24 | }
25 |
26 | if( ! this.props.disable_tracking ) {
27 | if( expanded ) {
28 | user_tracker.log_event({
29 | category: 'expand `'+this.props.header_text+'`',
30 | action: 'NA',
31 | });
32 | }
33 | }
34 | }
35 | render() {
36 | const header = (
37 |
42 |
43 | {' '}
44 | {this.state.expanded ? 'Close form' : this.props.header_text}
45 |
46 | );
47 |
48 | const body = (
49 |
50 | {this.props.body_content}
51 |
52 |
53 | );
54 |
55 | return (
56 |
60 | { header }
61 | { body }
62 |
63 | );
64 | }
65 | };
66 |
67 | export default CollapsableAdder;
68 |
--------------------------------------------------------------------------------
/client/src/js/components/mixins/collapse.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactCollapse from 'react-collapse';
3 |
4 |
5 | /*
6 | // works only with react-collapse@1.4.0
7 | const Collapse = React.createClass({
8 | getInitialState: () => ({never_rendered: true}),
9 | componentWillReceiveProps: function(nextProps) {
10 | if( nextProps.isOpened ) {
11 | this.setState({never_rendered: false});
12 | }
13 | },
14 | render: function(){
15 | if( this.state.never_rendered ) {
16 | return null;
17 | }
18 | return (
19 |
20 | {this.props.children}
21 |
22 | );
23 | },
24 | });
25 | */
26 |
27 | // @3.2.0 is buggy
28 | // using @2.3.3 instead
29 | const Collapse = React.createClass({
30 | getInitialState: function() {
31 | return {
32 | never_rendered: !this.props.isOpened,
33 | };
34 | },
35 | componentWillReceiveProps: function(nextProps) {
36 | if( nextProps.isOpened ) {
37 | this.setState({never_rendered: false});
38 | }
39 | },
40 | render: function(){
41 | return (
42 |
43 | {!this.state.never_rendered && this.props.children}
44 |
45 | );
46 | },
47 | });
48 |
49 |
50 | export default {
51 | component: Collapse,
52 | };
53 |
54 |
--------------------------------------------------------------------------------
/client/src/js/components/mixins/flip-list.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import FlipMove from 'react-flip-move';
3 |
4 |
5 | const FlipListMixin = props =>
6 |
7 | export default FlipListMixin;
8 |
--------------------------------------------------------------------------------
/client/src/js/components/mixins/render-canceler.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | class RenderCanceler extends React.Component {
4 | constructor(props) {
5 | super(props);
6 | }
7 | render() {
8 | return You shoud never see this <RenderCanceler/>
9 | }
10 | }
11 | RenderCanceler.displayName = 'RenderCanceler';
12 |
13 | export default RenderCanceler;
14 |
--------------------------------------------------------------------------------
/client/src/js/components/mixins/reply-layout/index.js:
--------------------------------------------------------------------------------
1 | import ReplyLayoutMixin from './reply-layout.jsx';
2 |
3 | if( typeof window !== 'undefined' ) require('./reply-layout.css');
4 |
5 | export default ReplyLayoutMixin;
6 |
--------------------------------------------------------------------------------
/client/src/js/components/mixins/reply-layout/reply-layout.css:
--------------------------------------------------------------------------------
1 | .css_reply_layout_body {
2 | padding-left: 22px;
3 |
4 | position: relative;
5 | /* line to communicate reply list */
6 | &::before {
7 | /*
8 | content: "";
9 | */
10 | display: block;
11 | border-style: solid;
12 | border-width: 0 0 0 3px;
13 | height: calc(100% - 4px);
14 | position: absolute;
15 | bottom: 0;
16 | left: 3px;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/client/src/js/components/pages/about.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import route_spec from '../../util/route_spec';
4 |
5 | import LinkMixin from '../mixins/link';
6 |
7 | import LandingPage from '../pages/landing';
8 |
9 |
10 | const AboutPage = props =>
11 |
12 |
Author
13 |
14 | The codebase is written by Romuald .
15 | Many catalogs have been created by Romuald which he maintains with the help of hundreds of contributors.
16 |
17 |
18 |
19 |
Vision
20 |
21 | The vision is to help you find libraries and to help open source authors reach their audience.
22 | Maybe even one day to help you find other kinds of things, e.g. Web Apps.
23 | For now the focus is on programming libraries.
24 |
25 |
26 | {/*
27 |
28 |
Roadmap
29 |
30 |
31 |
32 | Further catalogs
33 |
34 | {' '}to cover more frontend development areas.
35 |
36 |
37 | To be determined.
38 |
39 |
40 | */}
41 |
42 |
43 |
Contributions
44 |
45 | Feel free to contact Romuald if you want to participate.
46 |
47 |
48 |
49 |
Contact
50 |
51 | Romuald's contact informations are over{' '}
52 | there
53 | .
54 |
55 |
;
56 |
57 | export default {
58 | page_route_spec: route_spec.from_crossroads_spec('/about'),
59 | component: AboutPage,
60 | hide_sidebar: true,
61 | get_page_head: () => {
62 | const pg = LandingPage.get_page_head();
63 | pg.dont_index = true;
64 | return pg;
65 | },
66 | };
67 |
68 |
--------------------------------------------------------------------------------
/client/src/js/components/pages/landing/index.js:
--------------------------------------------------------------------------------
1 | if( typeof window !== 'undefined' ) require ('./landing.css');
2 | export * from './landing.jsx';
3 | export {default} from './landing.jsx';
4 |
--------------------------------------------------------------------------------
/client/src/js/components/pages/landing/landing.css:
--------------------------------------------------------------------------------
1 | .css_page_landing {
2 | text-align: center;
3 | }
4 | .css_page_landing_content {
5 | display: inline-block;
6 | width: 600px;
7 | margin-top: 100px;
8 | text-align: left;
9 | }
10 | .css_page_landing_logo {
11 | padding: 5px;
12 | margin-left: -5px;
13 | margin-top: 20px;
14 | margin-bottom: -10px;
15 | display: inline-block;
16 | }
17 |
--------------------------------------------------------------------------------
/client/src/js/components/pages/landing/landing.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import assert_soft from 'assertion-soft';
3 | import route_spec from '../../../util/route_spec';
4 |
5 | import Tag from '../../../thing/tag';
6 |
7 | import LogoDevarchy from '../../snippets/logo-devarchy';
8 |
9 | const LandingPage = props => {
10 | const TagPage = require('../../pages/tag').default;
11 | const tag_name = Tag.get_meta_list_name({meta_data: props.meta_data});
12 | const tag_page = TagPage.element({...props, route: {params: {tag_name}}});
13 | return (
14 |
15 |
16 | { tag_page }
17 |
18 |
19 | );
20 | };
21 |
22 | export default {
23 | page_route_spec: route_spec.from_crossroads_spec('/'),
24 | component: LandingPage,
25 | hide_sidebar: true,
26 | get_page_head: () => ({title: "Frontend Catalogs", description: "Catalogs of libraries for frontend development."}),
27 | };
28 |
--------------------------------------------------------------------------------
/client/src/js/components/pages/needs-and-libs/index.js:
--------------------------------------------------------------------------------
1 | if( typeof window !== 'undefined' ) require ('./needs-and-libs.css');
2 | export * from './needs-and-libs.jsx';
3 | export {default} from './needs-and-libs.jsx';
4 |
--------------------------------------------------------------------------------
/client/src/js/components/pages/needs-and-libs/needs-and-libs.css:
--------------------------------------------------------------------------------
1 | @import '../../../../css/layout.vars.css';
2 | @import '../../../../css/colors.vars.css';
3 |
4 | .sel_block_list {
5 | display: flex;
6 | flex-wrap: wrap;
7 | justify-content: space-between;
8 | color: #666;
9 | overflow: hidden;
10 | &:after {
11 | content: "";
12 | flex: auto;
13 | }
14 |
15 | margin-left: -20px;
16 | & > * {
17 | margin-left: 20px !important;
18 | }
19 |
20 | & > * {
21 | display: block;
22 | white-space: nowrap;
23 | }
24 |
25 | & .sel_topic {
26 | margin-bottom: 5px;
27 | }
28 | }
29 |
30 | .css_load_icon {
31 | pointer-events: none;
32 |
33 | padding-top: 1px;
34 | top: 1px;
35 | right: 0;
36 | width: 41px;
37 |
38 | height: 100%;
39 | display: flex;
40 | align-items: center;
41 |
42 | position: relative;
43 |
44 | transition: none;
45 | opacity: 0;
46 |
47 | &.css_load_icon__activated {
48 | transition: opacity 0.7s;
49 | opacity: 1;
50 | }
51 | &:not(.css_load_icon__activated) {
52 | position: absolute;
53 | }
54 | }
55 | .css_input_del {
56 | top: 0;
57 | right: 0;
58 | padding-right: 4px;
59 | padding-left: 4px;
60 |
61 | height: 100%;
62 | display: flex;
63 | align-items: center;
64 |
65 | transition: opacity 0.3s;
66 | opacity: 0;
67 |
68 | &.css_input_del__activated {
69 | cursor: pointer;
70 | transition: none;
71 | opacity: 1;
72 | }
73 | &:not(.css_input_del__activated) {
74 | position: absolute;
75 | pointer-events: none;
76 | }
77 | }
78 |
79 | .sel_section_title {
80 | font-weight: normal;
81 | /*
82 | color: #555;
83 | font-size: 1.25em;
84 | margin-bottom: 7px;
85 | */
86 | color: #aaa;
87 | font-size: 0.98em;
88 | margin-bottom: 3px;
89 |
90 | @nest
91 | .css_category & {
92 | margin-bottom: -1px;
93 | }
94 | }
95 |
96 | .sel_needs_header {
97 | width: 100%;
98 | position: relative;
99 |
100 | & .sel_needs_header_prefix {
101 | margin-left: 0;
102 | width: 0;
103 | min-width: calc(8px + var(--css_sidebar_width) - calc((100vw - 100%) / 2));
104 | }
105 |
106 | display: flex;
107 |
108 | & .sel_needs_header_body {
109 | flex-grow: 1;
110 | padding-right: 5px;
111 | padding-bottom: 35px;
112 | }
113 |
114 | & .sel_needs_header_title {
115 | padding-top: 16px;
116 | white-space: nowrap;
117 | & > * {
118 | display: inline-block;
119 | vertical-align: middle;
120 | }
121 |
122 | position: relative;
123 | &::before {
124 | content: '>';
125 | display: block;
126 | position: absolute;
127 | left: -38px;
128 | top: 30px;
129 | color: #ddd;
130 | font-size: 2.6em;
131 | }
132 | }
133 | & .sel_needs_search_box {
134 | margin-top: 29px;
135 |
136 | & input:focus ~ .sel_needs_search_box_overlay {
137 | opacity: 0;
138 | }
139 | & .sel_input_left_area:hover .sel_needs_search_box_overlay {
140 | opacity: 0;
141 | }
142 | }
143 | & .sel_catalog_logo {
144 | width: 70px !important;
145 | height: 70px !important;
146 | }
147 | & .css_catalog_title {
148 | padding-left: 7px;
149 | }
150 | }
151 |
152 | .sel_needs_search_box_overlay {
153 | pointer-events: none;
154 | position: absolute;
155 | background-color: var(--css_color_background);
156 | top: 0;
157 | left: 0;
158 | width: 100%;
159 | height: 100%;
160 | opacity: 1;
161 | transition: opacity .7s;
162 | white-space: nowrap;
163 | }
164 |
--------------------------------------------------------------------------------
/client/src/js/components/pages/not-found-404.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import LandingPage from '../pages/landing';
4 |
5 |
6 | const NotFoundPage = props => (
7 | {
14 | props.text ||
15 | 'Not Found...'
16 | }
17 | );
18 |
19 |
20 | export default {
21 | component: NotFoundPage,
22 | get_page_head: () => {
23 | const pg = LandingPage.get_page_head();
24 | pg.dont_index = true;
25 | pg.return_status_code = 404;
26 | return pg;
27 | },
28 | };
29 |
--------------------------------------------------------------------------------
/client/src/js/components/pages/redirect-catch.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import assert_soft from 'assertion-soft';
3 |
4 | import route_spec from '../../util/route_spec';
5 | import NotFoundPage from '../pages/not-found-404';
6 | import TagResourcePage from '../pages/tag-resource';
7 | import NeedsAndLibsPage from '../pages/needs-and-libs';
8 |
9 |
10 | const RedirectCatchPage = ({pathname}) => {
11 | if( typeof window !== "undefined" ) {
12 | const redirect_to = get_redirect_to(pathname);
13 | assert_soft(false, 'client-side redirection to '+redirect_to, pathname, window.location.href);
14 | if( assert_soft((pathname||'').startsWith('/'), pathname) ) {
15 | if( assert_soft(redirect_to, pathname) ) {
16 | setTimeout(() => {
17 | const navigation = require('../../navigation').default;
18 | navigation.navigate_to(redirect_to);
19 | }, 2000);
20 | return (
21 |
22 | );
23 | }
24 | }
25 | }
26 |
27 | return (
28 |
29 | );
30 | };
31 |
32 |
33 | export default {
34 | page_route_spec: route_spec({
35 | path_is_matching: ({pathname}) => !!get_redirect_to(pathname),
36 | interpolate_path: () => {assert_soft(false);},
37 | get_route_pattern: () => '/{redirected-route}',
38 | get_route_params: () => ({}),
39 | }),
40 | component: RedirectCatchPage,
41 | hide_sidebar: true,
42 | get_page_head: ({pathname}) => {
43 | const pg = NotFoundPage.get_page_head();
44 | const redirect_to = get_redirect_to(pathname);
45 | assert_soft(redirect_to, pathname);
46 | if( redirect_to ) {
47 | pg.redirect_to = redirect_to;
48 | pg.return_status_code = 301;
49 | }
50 | return pg;
51 | },
52 | };
53 |
54 |
55 | function get_redirect_to(pathname) {
56 | if( ! assert_soft((pathname||1).constructor===String, pathname) ) return null;
57 |
58 | const REDIRECTS = Object.entries({
59 | '/react-components': '/react',
60 | '/angular-components': '/angular',
61 | '/frontend-libraries': '/frontend',
62 | '/redirect-test-1': '/redirect-test-2',
63 | });
64 |
65 |
66 | for(const [source, target] of REDIRECTS) {
67 | const directory = (() => {
68 | if( ! TagResourcePage.page_route_spec.path_is_matching({pathname}) ) {
69 | return '';
70 | }
71 | if( ! NeedsAndLibsPage.page_route_spec.path_is_matching({pathname: target}) ) {
72 | return '';
73 | }
74 | const tag_page_params = TagResourcePage.page_route_spec.get_route_params({pathname});
75 | if( tag_page_params.tag_name !== source.slice(1) || !tag_page_params.resource_human_id ) {
76 | return '';
77 | }
78 | return '/library';
79 | })();
80 | if( pathname.startsWith(source) ) {
81 | return target+directory+pathname.slice(source.length);
82 | }
83 | }
84 |
85 | return null;
86 | }
87 |
88 |
89 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/button/button.css:
--------------------------------------------------------------------------------
1 | @import '../../../../css/icons.vars.css';
2 |
3 | :root {
4 | --css_button_transition_duration: 2s;
5 | --css_button_transition_duration__fast: 0.4s;
6 | }
7 |
8 | .css_button_text,
9 | .css_button_icon,
10 | .css_button_big {
11 | cursor: pointer;
12 | user-select: none;
13 | }
14 |
15 | .css_button_text,
16 | .css_button_icon {
17 | transition-property: color;
18 | transition-duration: var(--css_button_transition_duration);
19 | }
20 |
21 |
22 |
23 | .css_button_icon {
24 | display: inline-block;
25 | }
26 | .css_button_icon__icon {
27 | display: inline-flex;
28 | justify-content: center;
29 | align-items: center;
30 | vertical-align: middle;
31 |
32 | font-size: 1.18em;
33 | width: 1.94em;
34 | height: 1.94em;
35 |
36 | color: #888;
37 |
38 | @nest .css_button_icon.css_is_pressed & {
39 | background-color: #f9f9f9;
40 | }
41 |
42 | border-radius: 50%;
43 | border-width: 1px;
44 | border-style: solid;
45 | transition-property: border-color, color;
46 | transition-duration: var(--css_button_transition_duration);
47 | @nest .css_button_icon:not(.css_is_pressed) & {
48 | border-color: #888;
49 | }
50 | }
51 |
52 |
53 | .css_button_text {
54 | & svg {
55 | color: #888;
56 | }
57 | }
58 |
59 |
60 |
61 | .css_button_big {
62 | &, & > span {
63 | display: inline-block;
64 | }
65 | padding: 6px 10px;
66 | text-transform: uppercase;
67 | font-size: .8em;
68 | border-radius: 2px;
69 | background-clip: padding-box;
70 | letter-spacing: 1px;
71 | border: 1px solid rgba(0,0,0, 0.08);
72 | transition-property: color;
73 | }
74 |
75 |
76 |
77 | .css_saving *,
78 | .css_saving,
79 | [disabled],
80 | [disabled] * {
81 | &.css_button_text,
82 | &.css_button_icon {
83 | cursor: default;
84 | transition-duration: var(--css_button_transition_duration__fast);
85 | }
86 | &.css_button_text {
87 | color: #ccc;
88 | }
89 | &.css_button_icon {
90 | color: #888;
91 | & .css_button_icon__icon {
92 | color: #ddd;
93 | border-color: #ddd !important;
94 | transition-duration: var(--css_button_transition_duration__fast);
95 | }
96 | }
97 | }
98 |
99 |
100 |
101 | .css_saving .css_button_icon.css_is_async .css_button_icon__icon svg {
102 | color: transparent;
103 | }
104 | .css_saving [type="submit"],
105 | .css_saving .css_is_async,
106 | .css_saving {
107 | &.css_button_text,
108 | &.css_button_icon .css_button_icon__icon {
109 | @apply --css_loading_icon__middle;
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/button/index.js:
--------------------------------------------------------------------------------
1 | if( typeof window !== 'undefined' ) require ('./button.css');
2 | export * from './button.jsx';
3 | export {default} from './button.jsx';
4 |
5 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/catalog-title.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import assert_soft from 'assertion-soft';
3 |
4 |
5 | export const CatalogLogo = ({tag}) => {
6 | const {tag_logo} = tag.display_options;
7 |
8 | assert_soft(tag_logo, tag);
9 |
10 | return (
11 | tag_logo && (
12 |
24 | ) || null
25 | );
26 | };
27 |
28 | export const CatalogName = ({tag}) => {
29 | const {tag_title__multiline} = tag.display_options;
30 |
31 | return (
32 |
35 | {tag_title__multiline}
36 |
37 | );
38 | };
39 |
40 | const CatalogTitleSnippet = ({tag}) => {
41 | const logo = ;
42 | const text = ;
43 |
44 | return (
45 |
46 |
47 | { logo && (
48 |
49 | {logo}
50 |
51 | ) }
52 | { text }
53 |
54 |
55 | );
56 | };
57 |
58 |
59 | export default CatalogTitleSnippet;
60 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/category/category.css:
--------------------------------------------------------------------------------
1 | @import '../../../../css/colors.vars.css';
2 | @import '../../../../css/icons.vars.css';
3 |
4 | .css_category_header {
5 | @nest .css_depth_1 & {
6 | font-size: 1.75em;
7 | }
8 | @nest .css_depth_2 & {
9 | font-size: 1.45em;
10 | }
11 | @nest .css_depth_3 & {
12 | margin: 2px 0;
13 | font-size: 1.09em;
14 | }
15 | margin-top: 0;
16 | margin-bottom: 0;
17 | display: inline-block;
18 | font-weight: normal;
19 | }
20 |
21 | .css_header_breadcrumb {
22 | font-size: 0.8em;
23 | margin-bottom: -1px;
24 | opacity: 0.4;
25 | }
26 |
27 | .css_category .css_category {
28 | padding-left: 15px;
29 | }
30 |
31 | .css_header_description,
32 | .css_small_header_description {
33 | font-style: italic;
34 | margin-top: 0;
35 | }
36 | .css_header_description {
37 | padding-bottom: 5px;
38 | opacity: 0.6;
39 | white-space: pre-wrap;
40 | }
41 | .css_small_header_description {
42 | margin-bottom: 3px;
43 | opacity: 0.4;
44 | font-size: 0.9em;
45 | }
46 |
47 | .css_category_end {
48 | @nest .css_depth_1 & {
49 | padding-bottom: 45px;
50 | }
51 | @nest .css_depth_2 & {
52 | padding-bottom: 0;
53 | }
54 | @nest .css_depth_3 & {
55 | padding-bottom: 0;
56 | }
57 | }
58 | .css_category_resource_list_end {
59 | padding-bottom: 40px;
60 | }
61 |
62 | .css_category_resource_list_header {
63 | padding-top: 5px;
64 |
65 | & em {
66 | opacity: 0.6;
67 | }
68 | }
69 |
70 | .sel_resource_adder {
71 | margin-top: 3px;
72 | margin-left: -1px;
73 | }
74 |
75 | .css_hidden_note {
76 | cursor: pointer;
77 | opacity: 0.4;
78 | margin-left: 122px;
79 | margin-top: 2px !important;
80 | margin-left: 1px !important;
81 |
82 | & em {
83 | opacity: 0.4;
84 | }
85 | }
86 |
87 | .sel_resource_adder,
88 | .css_hidden_note {
89 | @nest
90 | .css_category__web_entries & {
91 | padding-left: 34px;
92 | }
93 | @nest
94 | .css_category__npm_entries & {
95 | padding-left: 101px;
96 | }
97 | }
98 |
99 | .sel_category_resources {
100 | position: relative;
101 |
102 | @nest
103 | .sel_is_computing & {
104 | overflow: hidden;
105 | @apply --css_loading_icon__middle;
106 | &::before {
107 | pointer-events: none;
108 | }
109 | }
110 |
111 | &::after {
112 | content: "";
113 |
114 | pointer-events: none;
115 |
116 | position: absolute;
117 | top: 0;
118 | left: 0;
119 | width: 100%;
120 | height: 100%;
121 |
122 | background-color: var(--css_color_background);
123 | /*
124 | transition: opacity 0.5s;
125 | */
126 | opacity: 0;
127 |
128 | @nest
129 | .sel_is_computing & {
130 | transition: opacity 0.3s;
131 | opacity: 0.7;
132 | }
133 | }
134 | }
135 |
136 | .sel_category_elaboration {
137 | color: #aaa;
138 | border-left: 4px solid #eee;
139 | padding-left: 5px;
140 | padding-top: 0;
141 | padding-bottom: 2px;
142 |
143 | & p {
144 | margin-top: 0;
145 | margin-bottom: 5px;
146 | &:last-child {
147 | margin-bottom: 0;
148 | }
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/category/index.js:
--------------------------------------------------------------------------------
1 | if( typeof window !== 'undefined' ) require('./category.css');
2 | export * from './category.jsx';
3 | export {default} from './category.jsx';
4 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/comment-list/comment-list.css:
--------------------------------------------------------------------------------
1 | .css_comment + .css_comment,
2 | .css_comment_replies {
3 | /*
4 | padding-top: 4px;
5 | */
6 | }
7 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/comment-list/comment-list.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import assert from 'assertion-soft';
4 |
5 | import CommentSnippet from './comment';
6 | import FlipListMixin from '../../mixins/flip-list';
7 | import {IconButton} from '../../snippets/button';
8 | // import GoComment from 'react-icons/lib/go/comment';
9 |
10 | import rerender from '../../../rerender';
11 |
12 | import Thing from '../../../thing';
13 |
14 |
15 | const CommentAdder = ({thing, style}) => {
16 | const comments = thing.commentable.comments;
17 | const disabled = comments.some(c => c.is_editing);
18 | return (
19 | {
22 | thing.commentable.add_comment();
23 | rerender.carry_out();
24 | }}
25 | style={style}
26 | icon={
27 |
28 | //
29 | }
30 | text={'Comment'}
31 | />
32 | );
33 | };
34 |
35 | const CommentList = ({thing, className, style}) => {
36 | const comments = thing.commentable.comments;
37 | return (
38 |
39 |
40 | {
41 | comments.reverse().map(comment =>
42 | )
46 | }
47 |
48 |
49 | );
50 | };
51 |
52 | export {CommentList, CommentAdder, CommentList as default};
53 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/comment-list/index.js:
--------------------------------------------------------------------------------
1 | if( typeof window !== 'undefined' ) require('./comment-list.css');
2 | export * from './comment-list.jsx';
3 | export {default} from './comment-list.jsx';
4 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/description-line/description-line.css:
--------------------------------------------------------------------------------
1 | .css_line_component {
2 | @nest
3 | & + & {
4 | margin-left: 9px;
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/description-line/description-line.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import assert from 'assertion-soft';
3 | import assert_soft from 'assertion-soft';
4 |
5 | import pretty_print from '../../../util/pretty_print';
6 |
7 | import {TextButtonSnippet} from '../../snippets/button';
8 |
9 |
10 | export const LineComp = props => ;
11 |
12 | export const DescriptionLineUpvotes = ({thing}) => {
13 | assert(thing, thing);
14 | assert(thing.votable, thing);
15 | const number_of_upvotes = thing.votable.upvote.number_of();
16 | if( number_of_upvotes===0 ) {
17 | return null;
18 | }
19 | const text = number_of_upvotes+' upvote'+(number_of_upvotes===1?'':'s');
20 | return { text } ;
21 | };
22 |
23 | export const DescriptionLineAuthor = ({thing}) => {'by '}{ thing.author_name } ;
24 |
25 | export const DescriptionLineAge = ({thing}) => {
26 | /*
27 | assert(!thing.is_new || thing.is_editing);
28 | assert(thing.created_at);
29 | */
30 | if( !thing.created_at ) {
31 | return null;
32 | }
33 | const text_age = pretty_print.age(thing.created_at, {verbose: true})+' ago';
34 | return {text_age} ;
35 | };
36 |
37 | export const DescriptionLineComments = ({thing}) => {
38 | assert(thing, thing);
39 | assert(thing.commentable, thing);
40 | const number_of_comments = thing.commentable.comments.filter(c => !c.is_editing).length;
41 | if( number_of_comments===0 ) {
42 | return null;
43 | }
44 | const text = number_of_comments+' comment'+(number_of_comments===1?'':'s');
45 | return { text } ;
46 | };
47 |
48 | export const DescriptionLineEdit = ({thing, element}) => {
49 | assert(thing);
50 | assert(element);
51 | if( thing.is_editing ) {
52 | return null;
53 | }
54 | if( ! thing.is_author ) {
55 | return null;
56 | }
57 | return (
58 |
59 | {
61 | if( ! thing.draft.author ) {
62 | thing.draft.author = Thing.things.logged_user.id;
63 | }
64 | assert_soft(thing.draft.author === Thing.things.logged_user.id);
65 | thing.draft.text = thing.draft.text || thing.text;
66 | thing.draft.explanation = thing.draft.explanation || thing.explanation;
67 | thing.is_editing = true;
68 | element.forceUpdate();
69 | }}
70 | text={'edit'}
71 | />
72 |
73 | );
74 | };
75 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/description-line/index.js:
--------------------------------------------------------------------------------
1 | if( typeof window !== 'undefined' ) require ('./description-line.css');
2 | export * from './description-line.jsx';
3 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/eraser.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Eraser = React.createClass({
4 | render: function(){
5 | return ;
6 | },
7 | });
8 |
9 | export default {
10 | component: Eraser,
11 | };
12 |
13 |
14 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/icon/icon.css:
--------------------------------------------------------------------------------
1 | .css_icon_plus, .css_icon_cross {
2 | vertical-align: middle;
3 | transition: transform 0.3s, right 0.3s;
4 | height: 1em;
5 | width: 1em;
6 | display: inline-flex;
7 | align-items: center;
8 | align-content: center;
9 | justify-content: center;
10 |
11 | &::before {
12 | content: "+";
13 | font-size: 1.5em;
14 | }
15 | position: relative;
16 | right: 0;
17 | }
18 | .css_icon_cross {
19 | transform: rotate(45deg);
20 | right: -1px;
21 | }
22 |
23 |
24 | .css_icon_minus {
25 | font-size: 1.5em;
26 | line-height: 0;
27 | margin-top: -3px;
28 | display: inline-block;
29 | &::before {
30 | content: "\2013";
31 | }
32 | }
33 |
34 | .css_rotatable {
35 | transition: transform 0.3s;
36 | }
37 | .css_rotate_45deg {
38 | transform: rotate(45deg);
39 | }
40 |
41 | .css_icon_devarchy {
42 | display: inline-block;
43 | vertical-align: middle;
44 | position: relative;
45 | &,
46 | &::before {
47 | border: 2px solid #7f7f7f;
48 | }
49 | /*
50 | &::before {
51 | position: absolute;
52 | display: block;
53 | content: "";
54 | top: 1px;
55 | right: 0px;
56 | height: 4px;
57 | width: 4px;
58 | }
59 | */
60 | height: 22px;
61 | width: 22px;
62 | }
63 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/icon/index.js:
--------------------------------------------------------------------------------
1 | if( typeof window !== 'undefined' ) require('./icon.css');
2 | export {default} from './icon.jsx';
3 | export * from './icon.jsx';
4 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/loading/css-loader/css-loader.css:
--------------------------------------------------------------------------------
1 | /* https://codepen.io/jczimm/pen/vEBpoL */
2 |
3 | :root {
4 | --css_loader_red:
5 | #bbb;
6 | --css_loader_blue:
7 | #aaa;
8 | --css_loader_green:
9 | #999;
10 | --css_loader_yellow:
11 | #888;
12 | }
13 | .circular {
14 | animation: rotate 2s linear infinite;
15 | transform-origin: center center;
16 | }
17 |
18 | .path {
19 | stroke-dasharray: 1, 200;
20 | stroke-dashoffset: 0;
21 | animation: dash 1.5s ease-in-out infinite, color 6s ease-in-out infinite;
22 | stroke-linecap: round;
23 | }
24 |
25 | @keyframes rotate {
26 | 100% {
27 | transform: rotate(360deg);
28 | }
29 | }
30 |
31 | @keyframes dash {
32 | 0% {
33 | stroke-dasharray: 1, 200;
34 | stroke-dashoffset: 0;
35 | }
36 | 50% {
37 | stroke-dasharray: 89, 200;
38 | stroke-dashoffset: -35px;
39 | }
40 | 100% {
41 | stroke-dasharray: 89, 200;
42 | stroke-dashoffset: -124px;
43 | }
44 | }
45 |
46 | @keyframes color {
47 | 100%,
48 | 0% {
49 | stroke: var(--css_loader_red);
50 | }
51 | 40% {
52 | stroke: var(--css_loader_blue);
53 | }
54 | 66% {
55 | stroke: var(--css_loader_green);
56 | }
57 | 80%,
58 | 90% {
59 | stroke: var(--css_loader_yellow);
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/loading/css-loader/css-loader.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | // https://codepen.io/jczimm/pen/vEBpoL
4 | const Loader = ({size}) => (
5 |
6 |
7 |
8 | );
9 |
10 | export default Loader;
11 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/loading/css-loader/index.js:
--------------------------------------------------------------------------------
1 | if( typeof window !== 'undefined' ) require('./css-loader.css');
2 | export * from './css-loader.jsx';
3 | export {default} from './css-loader.jsx';
4 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/loading/index.js:
--------------------------------------------------------------------------------
1 | export * from './loading.jsx';
2 | export {default} from './loading.jsx';
3 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/loading/loading.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | /*
4 | import Loader from './css-loader';
5 | /*/
6 | import Loader from './react-loader';
7 | //*/
8 |
9 |
10 | const LoadingSnippet = React.createClass({
11 | render: function(){
12 |
13 | const size = this.props.size || 44*(this.props.scale||1);
14 |
15 | const style = {
16 | overflow: 'hidden',
17 | };
18 |
19 | if( this.props.center_loader ) {
20 | Object.assign(style, {
21 | display: 'flex',
22 | minWidth: size,
23 | minHeight: size,
24 | width: '100%',
25 | height: '100%',
26 | position: 'absolute',
27 | justifyContent: 'center',
28 | alignItems: 'center',
29 | top: 0,
30 | left: 0,
31 | })
32 | } else {
33 | Object.assign(style, {display: 'block', textAlign: 'center'});
34 | }
35 |
36 | Object.assign(style, this.props.style);
37 |
38 | return (
39 |
43 |
44 |
45 | );
46 | }
47 | });
48 |
49 | export default {
50 | component: LoadingSnippet,
51 | };
52 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/loading/react-loader/index.js:
--------------------------------------------------------------------------------
1 | if( typeof window !== 'undefined' ) require('./react-loader.css');
2 | export * from './react-loader.jsx';
3 | export {default} from './react-loader.jsx';
4 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/loading/react-loader/react-loader.css:
--------------------------------------------------------------------------------
1 | /* fix `inline-style-prefixer`'s `display: flex` bug in `react-md-spinner` */
2 | .css_markdown_loader__react > * {
3 | display: flex !important;
4 | }
5 |
6 | /* We set the keyframes via CSS so that html coming from backend can properly be styled.
7 | Otherwise loader doesn't work until JS has loaded. */
8 | @keyframes __react-md-spinner-animation__root-rotate{to{transform: rotate(360deg);}}
9 | @keyframes __react-md-spinner-animation__fill-unfill-rotate{12.5%{transform: rotate(135deg);}25%{transform: rotate(270deg);}37.5%{transform: rotate(405deg);}50%{transform: rotate(540deg);}62.5%{transform: rotate(675deg);}75%{transform: rotate(810deg);}87.5%{transform: rotate(945deg);}to{transform: rotate(1080deg);}}
10 | @keyframes __react-md-spinner-animation__layer-1-fade-in-out{0%{opacity: 1;}25%{opacity: 1;}26%{opacity: 0;}89%{opacity: 0;}90%{opacity: 1;}to{opacity: 1;}}
11 | @keyframes __react-md-spinner-animation__layer-2-fade-in-out{0%{opacity: 0;}15%{opacity: 0;}25%{opacity: 1;}50%{opacity: 1;}51%{opacity: 0;}to{opacity: 0;}}
12 | @keyframes __react-md-spinner-animation__layer-3-fade-in-out{0%{opacity: 0;}40%{opacity: 0;}50%{opacity: 1;}75%{opacity: 1;}76%{opacity: 0;}to{opacity: 0;}}
13 | @keyframes __react-md-spinner-animation__layer-4-fade-in-out{0%{opacity: 0;}65%{opacity: 0;}75%{opacity: 1;}90%{opacity: 1;}to{opacity: 0;}}
14 | @keyframes __react-md-spinner-animation__left-spin{from{transform: rotate(130deg);}50%{transform: rotate(-5deg);}to{transform: rotate(130deg);}}
15 | @keyframes __react-md-spinner-animation__right-spin{from{transform: rotate(-130deg);}50%{transform: rotate(5deg);}to{transform: rotate(-130deg);}}
16 | /* avoid cssnano to think that the keyframes are unused and to discard them */
17 | .i_should_not_exist {
18 | animation-name:
19 | __react-md-spinner-animation__root-rotate,
20 | __react-md-spinner-animation__fill-unfill-rotate,
21 | __react-md-spinner-animation__layer-1-fade-in-out,
22 | __react-md-spinner-animation__layer-2-fade-in-out,
23 | __react-md-spinner-animation__layer-3-fade-in-out,
24 | __react-md-spinner-animation__layer-4-fade-in-out,
25 | __react-md-spinner-animation__left-spin,
26 | __react-md-spinner-animation__right-spin;
27 | }
28 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/loading/react-loader/react-loader.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import MDSpinner from 'react-md-spinner';
3 |
4 | const Loader = ({size}) => (
5 |
13 | );
14 |
15 | export default Loader;
16 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/login-required.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Thing from '../../thing';
4 |
5 | import AuthPage from '../pages/auth';
6 | import LinkMixin from '../mixins/link';
7 |
8 |
9 | const LoginRequired = React.createClass({
10 | render: function(){
11 | const DEBUG = typeof window !== "undefined" && window.location.host === 'localhost:8082';
12 | if( Thing.things.logged_user && !DEBUG ) {
13 | return null;
14 | }
15 |
16 | return (
17 |
25 | Log in {' '} {this.props.text}
26 |
27 | }
28 | />
29 | );
30 | }
31 | });
32 |
33 |
34 | export default {
35 | component: LoginRequired,
36 | };
37 |
38 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/logo-devarchy/index.js:
--------------------------------------------------------------------------------
1 | if( typeof window !== 'undefined' ) require ('./logo-devarchy.css');
2 | export * from './logo-devarchy.jsx';
3 | export {default} from './logo-devarchy.jsx';
4 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/logo-devarchy/logo-devarchy.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devarchy/website/31a9fcc23c8bf2c8f64a153b6959e4f38b0c37c9/client/src/js/components/snippets/logo-devarchy/logo-devarchy.css
--------------------------------------------------------------------------------
/client/src/js/components/snippets/logo-devarchy/logo-devarchy.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import {IconDevarchy} from '../../snippets/icon';
4 |
5 | const LogoDevarchy = ({className}) => (
6 |
7 |
8 | devarchy
9 |
10 | )
11 |
12 | export default LogoDevarchy;
13 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/logo-section/index.js:
--------------------------------------------------------------------------------
1 | if( typeof window !== 'undefined' ) require ('./logo-section.css');
2 | export * from './logo-section.jsx';
3 | export {default} from './logo-section.jsx';
4 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/logo-section/logo-section.css:
--------------------------------------------------------------------------------
1 | @import '../../../../css/layout.vars.css';
2 | @import '../../../../css/colors.vars.css';
3 |
4 | :root {
5 | --css_sidebar_left_margin: 21px;
6 | --css_logo_section_height: 100px;
7 | --css_logo_height: 64px;
8 | --css_logo_padding_top: 20px;
9 | --css_logo_section_height_expanded: calc(var(--css_logo_height) + 125px);
10 | }
11 |
12 | .css_logo_section {
13 | position: absolute;
14 | top: 0;
15 | left: 0;
16 | width: var(--css_sidebar_width);
17 | max-width: 100%;
18 | overflow-y: hidden;
19 | z-index: 1;
20 |
21 | border-style: solid;
22 | border-width: 0 0 3px 0;
23 | transition-property: height, background-color, border-color;
24 | transition-duration: 0.6s;
25 |
26 | background-color: var(--css_color_sidebar_background);
27 | @nest #js_logo_section_2 & {
28 | background-color: transparent;
29 | border-right-width: 3px;
30 | @nest
31 | .css_hide_sidebar &,
32 | .css_show_resource_view &
33 | {
34 | z-index: 2;
35 | }
36 | }
37 | border-color: transparent;
38 | height: var(--css_logo_height);
39 | &.css_expand {
40 | background-color: var(--css_color_sidebar_background) !important;
41 | border-color: var(--css_color_sidebar_border);
42 | height: var(--css_logo_section_height_expanded);
43 | }
44 | }
45 |
46 | .css_sidebar_content {
47 | padding-top: var(--css_logo_section_height);
48 | }
49 |
50 | .css_logo_wrapper {
51 | display: flex;
52 | align-items: flex-end;
53 | height: var(--css_logo_height);
54 | padding-left: var(--css_sidebar_left_margin);
55 | }
56 |
57 | .css_logo_menu {
58 | padding-top: 10px;
59 | justify-content: center;
60 | > :first-child {
61 | margin-top: -7px;
62 | }
63 |
64 | width: 100%;
65 | line-height: 1.6em;
66 |
67 | align-items: flex-start;
68 | padding-left: var(--css_sidebar_left_margin);
69 | }
70 |
71 | :root {
72 | --css_delay_duration: 0.7s;
73 | }
74 |
75 |
76 | .css_logo {
77 | font-size: 26px;
78 | }
79 | .css_logo_text {
80 | font-family: 'Cutive Mono';
81 | color: #777;
82 | font-size: 26px;
83 | margin-left: 5px;
84 | vertical-align: middle;
85 | }
86 |
87 | .css_logo_menu {
88 | position: relative;
89 | }
90 |
91 | .css_logo_links > * {
92 | color: #666 !important;
93 |
94 | & > * > :first-child {
95 | margin-left: 5px;
96 | margin-right: 6px;
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/markdown-list-view/index.js:
--------------------------------------------------------------------------------
1 | import MarkdownListView from './markdown-list-view.jsx';
2 |
3 | if( typeof window !== 'undefined' ) require ('./markdown-list-view.css');
4 |
5 | export default MarkdownListView;
6 |
7 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/markdown-list-view/markdown-list-view.css:
--------------------------------------------------------------------------------
1 | @import '../../../../css/colors.vars.css';
2 |
3 | .css_new_requests {
4 | padding-top: 3px;
5 | margin-top: 5px;
6 | margin-left: 6px;
7 | padding-left: 10px;
8 | border-width: 0 0 0 6px;
9 | border-color: var(--css_color_gray);
10 | border-style: solid;
11 | }
12 |
13 | .css_catalog_title {
14 | display: inline-block;
15 | vertical-align: middle;
16 | text-align: left;
17 | margin-right: 10px;
18 | white-space: pre;
19 | }
20 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/month-picker/index.js:
--------------------------------------------------------------------------------
1 | if( typeof window !== 'undefined' ) require ('./month-picker.css');
2 | export {default} from './month-picker.jsx';
3 | export * from './month-picker.jsx';
4 |
5 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/month-picker/month-picker.css:
--------------------------------------------------------------------------------
1 | .css_react_month_picker .overlay {
2 | display: none;
3 | }
4 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/month-picker/month-picker.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactMonthPicker from 'react-month-picker';
3 | import assert_hard from 'assert';
4 |
5 |
6 | { /*
7 | Alternative: https://github.com/YouCanBookMe/react-datetime
8 | */}
9 | class MonthPicker extends React.Component {
10 | render() {
11 | const value = {};
12 | if( this.props.defaultValue ) {
13 | const d = new Date(this.props.defaultValue);
14 | value.month = d.getMonth()+1;
15 | value.year = d.getFullYear();
16 | }
17 | return (
18 |
27 | {
32 | assert_hard(year);
33 | assert_hard(month);
34 | const month_year_string = year+'-'+month;
35 | assert_hard(new Date(month_year_string) != 'Invalid Date');
36 | this.props.onChange(month_year_string);
37 | this.refs["ref_picker"].dismiss();
38 | }}
39 | ref="ref_picker"
40 | onDismiss={() => {
41 | if( this.props.onClose ) {
42 | this.props.onClose();
43 | }
44 | }}
45 | />
46 |
47 | );
48 | }
49 | open() {
50 | this.refs["ref_picker"].show();
51 | }
52 | close() {
53 | this.refs["ref_picker"].dismiss();
54 | }
55 | };
56 |
57 |
58 | export default MonthPicker;
59 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/popular-board.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import classNames from 'classnames';
3 | import assert from 'assertion-soft';
4 |
5 | import Thing from '../../thing/thing';
6 | import Resource from '../../thing/resource';
7 | import Tag from '../../thing/tag';
8 |
9 | import ResourceListSnippet from '../snippets/resource-list';
10 |
11 |
12 | const PopularResourcesSnippet = ({i, text, resource_list_data, full_text_search_value, need__missing_words}) => {
13 | if( resource_list_data.resource_list.length === 0 ) {
14 | return null;
15 | }
16 |
17 | return (
18 |
21 | { text &&
{text} }
22 |
27 |
28 | );
29 | };
30 | PopularResourcesSnippet.get_props = ({tag, age_max, age_min, age_missing, text, i, resource_list, resource_requests, is_request, is_need_view}) => {
31 |
32 | const resources = (() => {
33 | if( is_request ) {
34 | return resource_requests;
35 | }
36 | return (
37 | Resource.list_things({
38 | age_min, age_max, age_missing,
39 | list: resource_list,
40 | })
41 | );
42 | })();
43 |
44 | return {
45 | i,
46 | text,
47 | resource_list_data: ResourceListSnippet.get_props({tag, resource_list: resources, is_request, is_need_view}),
48 | }
49 | };
50 |
51 |
52 | const PopularBoardSnippet = ({parts, full_text_search_value, need__missing_words}) => {
53 | return (
54 | {
55 | parts
56 | .map(part =>
57 | PopularResourcesSnippet(Object.assign(part, {full_text_search_value, need__missing_words}))
58 | )
59 | }
60 | );
61 | };
62 |
63 | export default {
64 | component: PopularBoardSnippet,
65 | get_props: ({tag, resource_list, resource_requests, is_need_view}) => {
66 | assert( tag );
67 | assert( tag.constructor === Tag );
68 | assert( tag.is_markdown_list );
69 |
70 | assert( resource_list );
71 | assert( resource_list.constructor === Array );
72 |
73 | assert( resource_requests );
74 | assert( resource_requests.constructor === Array );
75 |
76 | const ONE_YEAR = 365;
77 | const THREE_YEARS = ONE_YEAR*3;
78 | const SIX_YEARS = ONE_YEAR*6;
79 |
80 | let parts = (
81 | [
82 | {
83 | age_max: ONE_YEAR,
84 | text: "0-11 months old",
85 | },
86 | {
87 | age_min: ONE_YEAR,
88 | age_max: THREE_YEARS,
89 | text: "1-2 years old",
90 | },
91 | {
92 | age_min: THREE_YEARS,
93 | age_max: SIX_YEARS,
94 | text: "3-5 years old",
95 | },
96 | {
97 | age_min: SIX_YEARS,
98 | text: "6+ years old",
99 | },
100 | {
101 | age_missing: true,
102 | text: "Unknown age",
103 | },
104 | {
105 | is_request: true,
106 | text: "Awaiting upvotes",
107 | },
108 | ]
109 | );
110 |
111 | parts = parts.map((spec, i) =>
112 | PopularResourcesSnippet.get_props(
113 | Object.assign({tag, resource_list, resource_requests, is_need_view}, spec, {i})))
114 |
115 | return {
116 | parts,
117 | };
118 |
119 | },
120 | };
121 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/progressbar/index.js:
--------------------------------------------------------------------------------
1 | if( typeof window !== 'undefined' ) {
2 | require('./progressbar.css');
3 | }
4 | export {default} from './progressbar.js';
5 | export * from './progressbar.js';
6 |
7 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/progressbar/progressbar.css:
--------------------------------------------------------------------------------
1 | @import 'nprogress/nprogress.css';
2 |
3 | #nprogress {
4 | z-index: 99999;
5 | filter: grayscale(1) contrast(200%) brightness(1);
6 | }
7 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/progressbar/progressbar.js:
--------------------------------------------------------------------------------
1 | import NProgress from 'nprogress';
2 |
3 |
4 | NProgress.configure({
5 | minimum: 0.1,
6 | showSpinner: false,
7 | speed: 700,
8 | trickleSpeed: 100,
9 | });
10 |
11 | const start = () => {
12 | NProgress.start();
13 | };
14 | const done = () => {
15 | NProgress.done();
16 | };
17 | /*
18 | let queued = false;
19 | let is_done = true;
20 |
21 | const start = () => {
22 | if( !is_done ) {
23 | queued = true;
24 | return;
25 | }
26 | NProgress.start();
27 | is_done = false;
28 | };
29 | const done = () => {
30 | NProgress.done();
31 | setTimeout(() => {
32 | is_done = true;
33 | if( queued ) {
34 | queued = false;
35 | start();
36 | }
37 | }, 1000);
38 | };
39 | */
40 |
41 |
42 | export default {
43 | start,
44 | done,
45 | };
46 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/resource-details/index.js:
--------------------------------------------------------------------------------
1 | import ResourceDetailsSnippet from './resource-details.jsx';
2 |
3 | if( typeof window !== 'undefined' ) require('./resource-details.css');
4 |
5 | export default ResourceDetailsSnippet;
6 |
7 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/resource-details/resource-details.css:
--------------------------------------------------------------------------------
1 | .css_resource_details {
2 | padding: 0 10px;
3 | padding-top: 10px;
4 | padding-bottom: 40px;
5 | }
6 |
7 | .markdown-body {
8 | font-size: 15px!important;
9 | }
10 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/resource-line/index.js:
--------------------------------------------------------------------------------
1 | import ResourceLineSnippet from './resource-line.jsx';
2 |
3 | if( typeof window !== 'undefined' ) require ('./resource-line.css');
4 |
5 | export default ResourceLineSnippet;
6 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/resource-line/resource-line.css:
--------------------------------------------------------------------------------
1 | @import '../../../../css/icons.vars.css';
2 |
3 | .css_resource {
4 | display: block;
5 | width: 100%;
6 | overflow: hidden;
7 | white-space: nowrap;
8 | text-overflow: ellipsis;
9 | cursor: pointer;
10 | }
11 |
12 | .css_resource_header__stars {
13 | display: inline-block;
14 | width: 41px;
15 | text-align: right;
16 | &::after {
17 | color: #aaa;
18 | content: " \2605";
19 | }
20 | }
21 | .css_resource_header__date {
22 | display: inline-block;
23 | width: 50px;
24 | text-align: right;
25 | font-size: 0.9em;
26 | &::after {
27 | @apply --css_icon_age;
28 | background-repeat: no-repeat;
29 | display: inline-block;
30 | content: "";
31 | width: 13px;
32 | height: 13px;
33 | margin-left: 4px;
34 | /*
35 | margin-bottom: -2px;
36 | */
37 | margin-bottom: -1px;
38 |
39 | /*
40 | font-family: FontAwesome;
41 | color: #aaa;
42 | content: " \f271";
43 | */
44 | }
45 | }
46 | .css_resource_header__downvotes,
47 | .css_resource_header__upvotes {
48 | display: inline-block;
49 | width: 25px;
50 | padding-left: 5px;
51 | text-align: left;
52 | &::before {
53 | color: #aaa;
54 | font-size: 1.2em;
55 | }
56 | }
57 | .css_resource_header__upvotes {
58 | &::before {
59 | content: "\25B4 ";
60 | }
61 | }
62 | .css_resource_header__downvotes {
63 | &::before {
64 | content: "\25BE ";
65 | }
66 | }
67 | .css_resource_header__name {
68 | display: inline;
69 | padding-left: 10px;
70 | font-size: 1.05em;
71 | }
72 | .css_resource_header__description {
73 | display: inline;
74 | padding-left: 6px;
75 | color: #999;
76 | font-size: 13.33333px;
77 | }
78 |
79 |
80 | .css_resource_request {
81 | margin-bottom: 5px;
82 | }
83 | .css_resource_header__request_name {
84 | display: inline;
85 | font-size: 1.1em;
86 | color: #888;
87 | padding-left: 3px;
88 | font-size: 0.9em;
89 | }
90 | .css_resource_header__request_description {
91 | display: inline;
92 | font-size: 13.33333px;
93 | }
94 |
95 | .css_resource_header__description b {
96 | color: #666;
97 | }
98 | .css_resource_header__name b {
99 | color: #222;
100 | }
101 |
102 | .css_resource_dim {
103 | opacity: 0.3;
104 | }
105 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/resource-list.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import ResourceLineSnippet from '../snippets/resource-line';
4 |
5 |
6 | const ResourceListSnippet = React.createClass({
7 | propTypes: {
8 | resource_list: React.PropTypes.array.isRequired,
9 | },
10 | render: function() {
11 | if( this.props.resource_list.length === 0 ) {
12 | if( this.props.resources_none_component ) {
13 | return this.props.resources_none_component;
14 | }
15 | return null;
16 | }
17 |
18 | return (
19 | {
20 | this.props.resource_list
21 | .map(({key, props}) =>
22 |
28 | )
29 | }
30 | );
31 | },
32 | });
33 |
34 |
35 | export default {
36 | component: ResourceListSnippet,
37 | get_props: ({tag, resource_list, date_column_first, resources_none_component, addition_request_view, is_request, is_need_view}) => ({
38 | resource_list:
39 | resource_list.map(resource => ({
40 | key:resource.key,
41 | props: ResourceLineSnippet.get_props({tag, resource, date_column_first, addition_request_view, is_request, is_need_view}),
42 | })),
43 | resources_none_component,
44 | }),
45 | };
46 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/resource-request-list.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import ResourceLineSnippet from '../snippets/resource-line';
4 |
5 |
6 | const ResourceRequestListSnippet = React.createClass({
7 | propTypes: {
8 | resource_list: React.PropTypes.array.isRequired,
9 | },
10 | render: function() {
11 | return {
12 | this.props.resource_list
13 | .map(({key, props}) =>
14 |
18 | )
19 | }
;
20 | },
21 | });
22 |
23 |
24 | export default {
25 | component: ResourceRequestListSnippet,
26 | get_props: ({tag, resource_list}) => ({
27 | resource_list:
28 | resource_list.map(({resource, category_display_name, req_date}) => ({
29 | key: resource.key,
30 | props: ResourceLineSnippet.get_props({tag, resource, addition_request_view: true, category_display_name, req_date, is_request: true, is_need_view: false}),
31 | })),
32 | }),
33 | };
34 |
35 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/resource-view/index.js:
--------------------------------------------------------------------------------
1 | import ResourceViewSnippet from './resource-view.jsx';
2 |
3 | if( typeof window !== 'undefined' ) require('./resource-view.css');
4 |
5 | export default ResourceViewSnippet;
6 |
7 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/resource-view/resource-view.css:
--------------------------------------------------------------------------------
1 | @import '../../../../css/colors.vars.css';
2 |
3 | .sel_resource_view {
4 | background-color: var(--css_color_sidebar_background);
5 | }
6 |
7 | .sel_resource_view__scroll_area {
8 | height: 100%;
9 | overflow-y: scroll;
10 | }
11 |
12 | .sel_resource_view__close_button {
13 | position: absolute;
14 | top: -10px;
15 | overflow: hidden;
16 | right: 0;
17 | z-index: 99;
18 | color: #ddd;
19 | cursor: pointer;
20 | padding: 5;
21 | font-size: 2.5em;
22 | text-shadow:
23 | -1px -1px 0 white,
24 | -1px 1px 0 white,
25 | 1px -1px 0 white,
26 | 1px 1px 0 white,
27 | 0px 0px 1px white,
28 | 0px 0px 5px white;
29 | }
30 |
31 |
32 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/reviewpoint-list/index.js:
--------------------------------------------------------------------------------
1 | import ReviewpointListSnippet from './reviewpoint-list.jsx';
2 |
3 | if( typeof window !== 'undefined' ) require('./reviewpoint-list.css');
4 |
5 | export default ReviewpointListSnippet;
6 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/reviewpoint-list/reviewpoint-list.css:
--------------------------------------------------------------------------------
1 | @import '../../../../css/colors.vars.css';
2 |
3 | .css_reviewpoint_symbol {
4 | font-size: 0.89em;
5 | margin-right: 3px;
6 |
7 | /*
8 | &.css_reviewpoint_is_negative {
9 | color: var(--css_color_red);
10 | }
11 | &:not(.css_reviewpoint_is_negative) {
12 | color: var(--css_color_green);
13 | }
14 | */
15 | }
16 |
17 | .css_review_point_body {
18 | margin-left: -3px;
19 | padding-top: 10px;
20 | padding-left: 4px;
21 | padding-bottom: 25px;
22 |
23 | position: relative;
24 | &::before {
25 | position: absolute;
26 | display: block;
27 | content: "";
28 | top: 5px;
29 | left: -16px;
30 | width: 6px;
31 | background-color: var(--css_color_gray);
32 | height: calc(100% - 14px);
33 | }
34 | }
35 |
36 | .css_review_point_adders {
37 | margin-top: 15px;
38 | }
39 |
40 | .css_reviewpoint {
41 | margin: 5px 0;
42 | }
43 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/reviewpoint-list/reviewpoint-list.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import assert from 'assertion-soft';
4 |
5 | import Thing from '../../../thing';
6 | import rerender from '../../../rerender';
7 |
8 | import {IconPro, IconCon} from '../../snippets/icon';
9 | import FlipListMixin from '../../mixins/flip-list';
10 | import ButtonSnippet from '../../snippets/button';
11 | import ReviewpointSnippet from './review-point';
12 |
13 |
14 | const ReviewpointAdder = ({resource, is_negative, ...props}) => (
15 | {
17 | resource.commentable.add_reviewpoint({is_negative});
18 | rerender.carry_out();
19 | }}
20 | icon_position={'right'}
21 | text={'Add'}
22 | {...props}
23 | />
24 | );
25 |
26 | const ReviewpointListSnippet = ({resource}) => {
27 | assert(resource);
28 | assert(resource instanceof Thing);
29 | assert(resource.type==='resource');
30 |
31 | const reviewpoints = resource.commentable.reviewpoints;
32 | const disable_adder_button = reviewpoints.some(c => c.is_editing);
33 |
34 | return (
35 |
36 |
37 | { reviewpoints.map(review_point => ) }
38 |
39 |
40 | }
44 | alt={'Add Pro'}
45 | disabled={disable_adder_button}
46 | />
47 | }
51 | alt={'Add Con'}
52 | disabled={disable_adder_button}
53 | />
54 |
55 |
56 | );
57 | };
58 |
59 | export default {
60 | component: ReviewpointListSnippet,
61 | };
62 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/social-buttons/index.js:
--------------------------------------------------------------------------------
1 | if( typeof window !== 'undefined' ) require ('./social-buttons.css');
2 | export * from './social-buttons.jsx';
3 | export {default} from './social-buttons.jsx';
4 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/social-buttons/social-buttons.css:
--------------------------------------------------------------------------------
1 | @import '../../../../css/colors.vars.css';
2 |
3 |
4 | .css_social__twitter,
5 | .css_social__github {
6 | filter: grayscale(1) brightness(105%) contrast(100%);
7 | opacity: 0;
8 | line-height: 0;
9 | transition: opacity 2s, filter 0.7s;
10 | &:hover {
11 | filter: grayscale(0) brightness(100%) contrast(100%);
12 | }
13 | &.css_reveal {
14 | opacity: 1;
15 | }
16 | }
17 |
18 | .css_social__loading_cover {
19 | width: 100%;
20 | height: 100%;
21 | position: absolute;
22 | z-index: 1;
23 | opacity: 1;
24 | transition: opacity 1s;
25 | display: flex;
26 | align-items: center;
27 | justify-content: center;
28 | background-color: var(--css_color_background);
29 | &.css_conceil {
30 | opacity: 0;
31 | pointer-events: none;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/tag-list/index.js:
--------------------------------------------------------------------------------
1 | if( typeof window !== 'undefined' ) require ('./tag-list.css');
2 | export * from './tag-list.jsx';
3 | export {default} from './tag-list.jsx';
4 |
5 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/tag-list/tag-list.css:
--------------------------------------------------------------------------------
1 | .css_sidebar_content {
2 | position: relative;
3 | }
4 | .css_sidebar_loading_icon {
5 | pointer-events: none;
6 | position: absolute;
7 | left: calc(50% - 12px);
8 | top: 129px;
9 | }
10 |
11 | .css_sidebar_loading_icon {
12 | opacity: 0;
13 | transition: opacity 0.9s;
14 | will-change: opacity;
15 | /*
16 | transform: translateZ(0);
17 | */
18 | @nest .css_sidebar_loading & {
19 | opacity: 1;
20 | }
21 | }
22 | .css_tag_list_content {
23 | opacity: 1;
24 | transition: opacity 0.9s;
25 | will-change: opacity, pointer-events;
26 | /*
27 | transform: translateZ(0);
28 | */
29 | @nest .css_sidebar_loading & {
30 | pointer-events: none;
31 | opacity: 0.3;
32 | }
33 | }
34 |
35 | /* required for react-scroll */
36 | #sel_scroll_area {
37 | position: relative;
38 | }
39 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/tag-resources-view.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Resource from '../../thing/resource';
4 |
5 | import ResourceListSnippet from '../snippets/resource-list';
6 |
7 | const NEW_LIMIT = 300;
8 |
9 | const Header = props => {props.text} ;
10 |
11 | const TagResourcesViewSnippet = props =>
12 |
13 |
14 |
{props.tag.name}
15 |
{props.tag.number_of_entries} entries
16 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
;
33 |
34 |
35 | export default {
36 | component: TagResourcesViewSnippet,
37 | get_props: ({tag}) => {
38 | const resource_list = Resource.list_things({tags: [tag]});
39 | return {
40 | tag: {
41 | name: tag.name,
42 | number_of_entries: resource_list.length,
43 | },
44 | resource_list__popular:
45 | ResourceListSnippet.get_props({
46 | tag,
47 | resource_list,
48 | is_need_view: false,
49 | }),
50 | resource_list__new:
51 | ResourceListSnippet.get_props({
52 | tag,
53 | resource_list: Resource.list_things({tags: [tag], order: {newest: true}}).slice(0, NEW_LIMIT),
54 | is_need_view: false,
55 | }),
56 | }
57 | },
58 | };
59 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/topic/index.js:
--------------------------------------------------------------------------------
1 | if( typeof window !== 'undefined' ) require('./topic.css');
2 | export {default} from './topic.jsx';
3 | export * from './topic.jsx';
4 |
5 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/topic/topic.css:
--------------------------------------------------------------------------------
1 | .sel_topic {
2 | font-size: 0.9em;
3 | border: 1px solid #aaa;
4 | border-radius: 2px;
5 | padding: 0px 7px;
6 | background: #fafafa;
7 | @nest
8 | & + & {
9 | margin-left: 7px;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/topic/topic.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import assert_soft from 'assertion-soft';
3 |
4 | import text_search from '../../../util/text_search';
5 | import Tag from '../../../thing/tag';
6 |
7 | import LinkMixin from '../../mixins/link';
8 | import NeedsAndLibsPage from '../../pages/needs-and-libs';
9 |
10 | const TopicSnippet = ({catalog_name, topic_name, full_text_search_value, topic_search_value}) => {
11 | if( ! assert_soft(catalog_name) ) return null;
12 | if( ! assert_soft(topic_name) ) return null;
13 |
14 | const children = (() => {
15 | if( topic_search_value && topic_search_value===topic_name ) {
16 | return text_search.hightlight_text(topic_name);
17 | }
18 | if( full_text_search_value ) {
19 | return text_search.highlight_search_match(topic_name, full_text_search_value);
20 | }
21 | return (
22 | topic_name
23 | );
24 | })();
25 |
26 | return (
27 |
36 | );
37 | };
38 |
39 | export default TopicSnippet;
40 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/user.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Thing from '../../thing';
3 |
4 | const UserSnippet = React.createClass({
5 | render: function(){
6 | const thing_user = Thing.things.id_map[this.props.user_id];
7 |
8 | const username = thing_user.user_name;
9 | const avatar = thing_user.user_image;
10 |
11 | return
12 | { avatar &&
}
13 | { avatar && ' ' }
14 |
{username}
15 |
;
16 | },
17 | });
18 |
19 | export default {
20 | component: UserSnippet,
21 | };
22 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/userinfo.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import assert from 'assertion-soft';
3 | import Promise from 'bluebird';
4 | import Thing from '../../thing';
5 |
6 | import AuthPage from '../pages/auth';
7 | import UserSnippet from '../snippets/user';
8 | import {BigButtonSnippet} from '../snippets/button';
9 |
10 | import GoMarkGithub from 'react-icons/lib/go/mark-github';
11 | Promise.longStackTraces();
12 |
13 |
14 | const LoginButton = () =>
15 |
19 |
22 | Login
23 |
24 |
25 |
26 | const UserinfoSnippet = React.createClass({
27 | render: function(){
28 | const logged_user = Thing.things.logged_user;
29 |
30 | // we need this because we render the logo section while fetching
31 | if( this.props.is_fetching_data ) {
32 | return null;
33 | }
34 |
35 | if( ! logged_user ) {
36 | return
37 | }
38 |
39 | assert( logged_user.id );
40 |
41 | return ;
42 | }
43 | });
44 |
45 | export default {
46 | component: UserinfoSnippet,
47 | fetch: () => {
48 | if( typeof window === "undefined" ) {
49 | return Promise.resolve();
50 | }
51 | return (
52 | Thing.load.logged_user()
53 | .then(() => {})
54 | );
55 | },
56 | };
57 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/vote-block/index.js:
--------------------------------------------------------------------------------
1 | if( typeof window !== 'undefined' ) require ('./vote-block.css');
2 | export * from './vote-block.jsx';
3 | export {default} from './vote-block.jsx';
4 |
--------------------------------------------------------------------------------
/client/src/js/components/snippets/vote-block/vote-block.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devarchy/website/31a9fcc23c8bf2c8f64a153b6959e4f38b0c37c9/client/src/js/components/snippets/vote-block/vote-block.css
--------------------------------------------------------------------------------
/client/src/js/navigation.js:
--------------------------------------------------------------------------------
1 | import assert from 'assertion-soft';
2 | import timerlog from 'timerlog';
3 | import { createHistory, createHashHistory } from 'history';
4 |
5 | // Using history@2 and not history@3 because of listeners being called twice
6 |
7 | assert(
8 | typeof window !== 'undefined',
9 | 'this module handles URL changes, it is therefore meant to be run in the browser');
10 |
11 |
12 | const history = window.location.host==='localhost:8082' && createHashHistory() || createHistory();
13 |
14 | const navigation = {
15 | update_location: pathname => history.replace({pathname}),
16 | navigate_to: pathname => history.push({pathname}),
17 | // history@3 doesn't initial call listeners
18 | // current: history.getCurrentLocation().pathname,
19 | current: null,
20 | on_change: null,
21 | user_has_not_navigated_yet: true,
22 | commit_path_to_browser_history,
23 | };
24 |
25 | let not_commited_to_browser_history = false
26 |
27 | history.listen(({pathname, action}) => {
28 | assert(pathname.constructor === String)
29 |
30 | not_commited_to_browser_history = action==='REPLACE';
31 |
32 | if( action==='REPLACE' ) {
33 | navigation.current = pathname;
34 | return;
35 | }
36 |
37 | if( navigation.current === pathname ) {
38 | return;
39 | }
40 |
41 | if( navigation.current !== null ) { // not needed for history@3
42 | navigation.user_has_not_navigated_yet = false;
43 | }
44 |
45 | timerlog({tag:'dataflow', message: 'User navigated to '+pathname});
46 |
47 | navigation.current = pathname;
48 |
49 | if( navigation.on_change ) navigation.on_change();
50 | });
51 |
52 | function commit_path_to_browser_history() {
53 | if( ! not_commited_to_browser_history ) {
54 | return false;
55 | }
56 | navigation.navigate_to(navigation.current);
57 | return true;
58 | }
59 |
60 | export default navigation;
61 |
--------------------------------------------------------------------------------
/client/src/js/rerender.js:
--------------------------------------------------------------------------------
1 | import assert from 'assertion-soft';
2 |
3 | export default {
4 | carry_out: function(){
5 | assert(this.action);
6 | this.action();
7 | },
8 | action: null,
9 | };
10 |
--------------------------------------------------------------------------------
/client/src/js/router.js:
--------------------------------------------------------------------------------
1 | import assert from 'assertion-soft';
2 | import timerlog from 'timerlog';
3 | import assert_soft from 'assertion-soft';
4 |
5 | import NotFound404Page from './components/pages/not-found-404';
6 | import LandingPage from './components/pages/landing';
7 | import AuthPage from './components/pages/auth';
8 | import TagPage from './components/pages/tag';
9 | import TagResourcePage from './components/pages/tag-resource';
10 | import AboutPage from './components/pages/about';
11 | import RedirectCatchPage from './components/pages/redirect-catch';
12 | import NeedsAndLibsPage from './components/pages/needs-and-libs';
13 |
14 |
15 | const all_pages = [
16 | LandingPage,
17 | AuthPage,
18 | AboutPage,
19 | RedirectCatchPage,
20 | NeedsAndLibsPage,
21 | TagPage,
22 | TagResourcePage,
23 | ];
24 | assert(all_pages.every(component => !!component.page_route_spec.path_is_matching));
25 | assert(all_pages.concat(NotFound404Page).every(component => !!component.get_page_head));
26 |
27 | const router = {
28 | get_route,
29 | get_pattern,
30 | };
31 |
32 | export default router;
33 |
34 |
35 | function get_route(pathname){
36 | assert((pathname||0).constructor===String, pathname);
37 |
38 | const component = (
39 | all_pages.find(component => component.page_route_spec.path_is_matching({pathname}))
40 | || NotFound404Page
41 | );
42 |
43 | const params = (component.page_route_spec||{}).get_route_params && component.page_route_spec.get_route_params({pathname}) || {};
44 |
45 | // timerlog({tags:['client', 'dataflow'], message: 'path `'+pathname+'` routed to `'+component.component.displayName+'`'});
46 |
47 | return {
48 | component,
49 | params,
50 | };
51 | }
52 |
53 |
54 | function get_pattern(pathname) {
55 | const route = router.get_route(pathname);
56 | if( assert_soft(route, pathname) ) {
57 | const component = route.component;
58 | if( assert_soft(component, route) ) {
59 | const route_spec = component.page_route_spec;
60 | if( assert_soft(route_spec, component) ) {
61 | const pattern = route_spec.get_route_pattern();
62 | assert_soft(pattern, route_spec);
63 | return pattern;
64 | }
65 | }
66 | }
67 | return null;
68 | }
69 |
--------------------------------------------------------------------------------
/client/src/js/thing/comment.js:
--------------------------------------------------------------------------------
1 | import Thing from './thing.js';
2 | import CommentableMixin from './mixins/commentable';
3 | import VotableMixin from './mixins/votable';
4 |
5 | class Comment extends VotableMixin(CommentableMixin(Thing), {author_can_selfvote: false}) {};
6 | Comment.type = 'comment'; // UglifyJS2 mangles class name
7 | Comment.props_immutable = Thing.props_immutable.concat(['referred_resource', 'referred_thing', 'author',]);
8 | Comment.props_required = Comment.props_immutable;
9 | export default Comment;
10 |
--------------------------------------------------------------------------------
/client/src/js/thing/genericvote.js:
--------------------------------------------------------------------------------
1 | import Thing from './thing.js';
2 |
3 | class Genericvote extends Thing {
4 | constructor(...args) {
5 |
6 | /*
7 | // temporary patch to make is_negative a required prop
8 | if( args[0].is_negative === undefined && (args[0].draft||{}).is_negative === undefined ) {
9 | if( Object.keys(args[0].draft||{}).length>0 ) {
10 | args[0].draft.is_negative = false;
11 | } else {
12 | args[0].is_negative = false;
13 | }
14 | }
15 | */
16 |
17 | super(...args);
18 | }
19 | };
20 | Genericvote.type = 'genericvote'; // UglifyJS2 mangles class name
21 | Genericvote.props_immutable = Thing.props_immutable.concat(['referred_thing', 'vote_type', 'is_negative', 'author', ]);
22 | Genericvote.props_required = Genericvote.props_immutable;
23 | export default Genericvote;
24 |
25 |
--------------------------------------------------------------------------------
/client/src/js/thing/index.js:
--------------------------------------------------------------------------------
1 | import Thing from './thing';
2 |
3 | export default Thing;
4 |
--------------------------------------------------------------------------------
/client/src/js/thing/mixins/commentable.js:
--------------------------------------------------------------------------------
1 | import assert from 'assertion-soft';
2 | import Thing from '../thing';
3 | import assert_soft from 'assertion-soft';
4 |
5 |
6 | const created_at__parsed = Symbol();
7 |
8 | const upvotes_count = Symbol();
9 |
10 | export default cls => class extends cls {
11 | // - note that the returned objects are re-created on each property access
12 | // - in case of computation issues; memoize returned object
13 | get commentable () {
14 | const thing = this;
15 | return {
16 | add_reviewpoint ({is_negative}) {
17 | assert([true, false].includes(is_negative));
18 | assert(thing.type==='resource');
19 | assert(thing.id);
20 | comment_thing(thing, Object.assign({
21 | type: 'reviewpoint',
22 | referred_resource: thing.id,
23 | }, {is_negative}));
24 | },
25 | add_comment () {
26 | assert(['resource', 'comment', 'reviewpoint', ].includes(thing.type));
27 | const referred_resource = thing.type==='resource' ? thing.id : thing.referred_resource;
28 | assert(referred_resource);
29 | comment_thing(thing, {
30 | type: 'comment',
31 | referred_thing: thing.id,
32 | referred_resource,
33 | });
34 | },
35 | get reviewpoints () {
36 | return (
37 | get_notes(
38 | thing,
39 | {
40 | type: 'reviewpoint',
41 | referred_resource: thing.id,
42 | }
43 | )
44 | );
45 | },
46 | get comments () {
47 | return (
48 | get_notes(
49 | thing,
50 | {
51 | type: 'comment',
52 | referred_thing: thing.id,
53 | }
54 | )
55 | );
56 | },
57 | };
58 | }
59 |
60 | constructor(...args) {
61 | super(...args);
62 |
63 | Object.defineProperty(this, 'is_editing', {
64 | value: false,
65 | writable: true,
66 | enumerable: false,
67 | configurable: false,
68 | });
69 | }
70 |
71 | get [created_at__parsed]() {
72 | if( this.created_at ) {
73 | return new Date(this.created_at);
74 | }
75 | if( this.is_new ) {
76 | return null;
77 | }
78 | assert(false);
79 | }
80 |
81 | get [upvotes_count]() {
82 | return this.votable.upvote.number_of();
83 | }
84 |
85 | static order() {
86 | return ['-is_new', {key: upvotes_count}, {to_negate: true, key: created_at__parsed}];
87 | }
88 | };
89 |
90 | function comment_thing(thing, props) {
91 | assert( Thing.things.logged_user );
92 | assert( Thing.things.logged_user.id );
93 | assert( thing instanceof Thing );
94 | assert( thing.id );
95 |
96 | const author = Thing.things.logged_user.id;
97 |
98 | const comment_creator = Thing.get_or_create(Object.assign(props, {is_new: true, author}));
99 |
100 | comment_creator.is_editing = true;
101 | }
102 |
103 | function get_notes(thing, props) {
104 | if( !thing.id ) return [];
105 |
106 | const comment_list = (
107 | Thing
108 | .get_things_from_cache(props)
109 | .filter(t => !t.is_new || t.is_editing)
110 | .filter(t => !t.is_removed)
111 | );
112 |
113 | assert_soft(comment_list.every(t => t.is_editing===true || t.created_at), comment_list, comment_list.map(c => c.is_new));
114 |
115 | return Thing.sort(comment_list);
116 | }
117 |
--------------------------------------------------------------------------------
/client/src/js/thing/reviewpoint.js:
--------------------------------------------------------------------------------
1 | import assert from 'assertion-soft';
2 | import Thing from './thing.js';
3 | import CommentableMixin from './mixins/commentable';
4 | import VotableMixin from './mixins/votable';
5 |
6 |
7 | class Reviewpoint extends VotableMixin(CommentableMixin(Thing), {author_can_selfvote: true}) {
8 | get is_a_negative_point() {
9 | assert([this.is_negative, this.draft.is_negative].every(val => [undefined, true, false].includes(val)));
10 | if( this.is_negative !== undefined ) {
11 | return this.is_negative;
12 | }
13 | if( this.draft.is_negative !== undefined ) {
14 | return this.draft.is_negative;
15 | }
16 | }
17 | };
18 | Reviewpoint.type = 'reviewpoint'; // UglifyJS2 mangles class name
19 | Reviewpoint.props_immutable = Thing.props_immutable.concat(['referred_resource', 'author', ]);
20 | Reviewpoint.props_required = Reviewpoint.props_immutable;
21 | export default Reviewpoint;
22 |
--------------------------------------------------------------------------------
/client/src/js/thing/user.js:
--------------------------------------------------------------------------------
1 | import assert_soft from 'assertion-soft';
2 | import Thing from './thing.js';
3 |
4 | class User extends Thing {
5 | get user_name() {
6 | return get_provider_info(this).name;
7 | }
8 | get user_provider_and_name() {
9 | return get_provider_info(this).provider_and_name;
10 | }
11 | get user_image() {
12 | return get_provider_info(this).avatar;
13 | }
14 | };
15 | User.type = 'user'; // UglifyJS2 mangles class name
16 | export default User;
17 |
18 | function get_provider_info(user) {
19 | const PROVIDERS = [
20 | {
21 | prop: 'github_login',
22 | get_name: user => (user.github_info||{}).login,
23 | get_avatar: user => (user.github_info||{}).avatar_url,
24 | },
25 | {
26 | prop: 'facebook_user_id',
27 | get_name: user => user.facebook_info.name,
28 | get_avatar: user => 'https://graph.facebook.com/'+user.facebook_user_id+'/picture',
29 | },
30 | {
31 | prop: 'twitter_user_id',
32 | get_name: user => user.twitter_info.screen_name,
33 | get_avatar: user => user.twitter_info.profile_image_url_https,
34 | },
35 | ];
36 | const provs = (
37 | PROVIDERS
38 | .filter(({prop}) => user[prop])
39 | .map(provider => ({
40 | get provider_and_name() {
41 | const name = this.name;
42 | assert_soft(name);
43 | return provider.prop + '->' + name;
44 | },
45 | get name() {
46 | return provider.get_name(user);
47 | },
48 | get avatar() {
49 | return provider.get_avatar(user);
50 | },
51 | }))
52 | );
53 | assert_soft(provs.length>0, user);
54 | assert_soft(provs.length===1, user);
55 | return provs[0];
56 | }
57 |
--------------------------------------------------------------------------------
/client/src/js/util/debouncer.js:
--------------------------------------------------------------------------------
1 | const assert_soft = require('assertion-soft');
2 |
3 | module.exports = debouncer;
4 |
5 | function debouncer(fct, {time}) {
6 | assert_soft(time>=0);
7 |
8 | let timeout = null;
9 |
10 | return fct__debounced;
11 |
12 | function fct__debounced() {
13 | clearTimeout(timeout);
14 | timeout = setTimeout(() => {
15 | timeout = null;
16 | fct.apply(this, arguments);
17 | }, time);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/client/src/js/util/http.js:
--------------------------------------------------------------------------------
1 | import assert from 'assertion-soft';
2 | import fetch from 'isomorphic-fetch';
3 | import Promise from 'bluebird';
4 | import timerlog from 'timerlog';
5 | import ExtendableError from 'extendable-error-class';
6 | Promise.longStackTraces();
7 |
8 |
9 | const http = function({uri, method, body, json, qs, withCredentials = true, }) {
10 | const headers = {};
11 |
12 | if( json ) {
13 | body = JSON.stringify(body);
14 | headers['Accept'] = 'application/json';
15 | headers['Content-Type'] = 'application/json';
16 | }
17 |
18 | if( qs ) {
19 | uri += build_query_string(qs);
20 | qs = null;
21 | }
22 |
23 | const log_id = timerlog({
24 | disable: false,
25 | tags: ['client', 'performance'],
26 | start_timer: true,
27 | message: method+' '+uri,
28 | });
29 | return (
30 | new Promise((resolve, reject) => {
31 | fetch( uri, {
32 | method,
33 | headers,
34 | body,
35 | credentials: withCredentials ? 'include' : 'same-origin',
36 | })
37 | .then(resp => resolve(resp))
38 | .catch(err => {
39 | console.log('Error while fetching `'+method+' '+uri+'`');
40 | reject(err);
41 | });
42 | })
43 | )
44 | .finally(() => {timerlog({id: log_id, end_timer: true})})
45 | .then(resp => process_response(resp, method, json))
46 | };
47 |
48 |
49 | class HttpError extends ExtendableError {
50 | constructor(response) {
51 |
52 | const message =
53 | (response.body||{}).message
54 | ||
55 | 'HttpError: ' +
56 | [
57 | response.status,
58 | response.method,
59 | response.url,
60 | response.statusText,
61 | ].filter(v => !!v).join(' ');
62 |
63 | super(message);
64 |
65 | Object.assign(this, response);
66 | this.message = message;
67 |
68 | }
69 | toString() { return this.message }
70 | };
71 |
72 |
73 | http.HttpError = HttpError;
74 | export default http;
75 |
76 |
77 | function process_response(response, method, json) {
78 | const response_copy = {};
79 | [
80 | 'status',
81 | 'statusText',
82 | 'url',
83 | 'method',
84 | ]
85 | .forEach(p => response_copy[p] = response[p]);
86 |
87 | return (
88 | Promise.resolve()
89 | )
90 | .then(() => {
91 | if( !json ) {
92 | return;
93 | }
94 | return (
95 | response.json()
96 | .then(response_body => {response_copy.body = response_body})
97 | .catch(err => {
98 | console.log('Error while parsing json for `'+method+' '+response_copy.url+'`');
99 | throw err;
100 | })
101 | );
102 | })
103 | .then(() => {
104 | if (response_copy.status >= 200 && response_copy.status < 300) {
105 | return response_copy.body;
106 | } else {
107 | throw new HttpError(response_copy);
108 | }
109 | });
110 | }
111 |
112 | function build_query_string(params) {
113 | const str = [];
114 | for(let p in params) {
115 | if (params.hasOwnProperty(p)) {
116 | str.push(encodeURIComponent(p) + "=" + encodeURIComponent(params[p]));
117 | }
118 | }
119 | return '?'+str.join("&");
120 | }
121 |
--------------------------------------------------------------------------------
/client/src/js/util/normalize_url.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 |
4 | module.exports = normalize_url;
5 | normalize_url.is_url = is_url;
6 | normalize_url.remove_http_protocol = remove_http_protocol;
7 | normalize_url.ensure_protocol_existence = ensure_protocol_existence;
8 |
9 |
10 | const URL_PROTOCOL_REGEX = /https?\:\/\//;
11 |
12 | function is_url(str) {
13 | return includes_protocol(str);
14 | }
15 |
16 | function includes_protocol(str) {
17 | return URL_PROTOCOL_REGEX.test(str);
18 | }
19 |
20 | function normalize_url(str) {
21 | if( !str ) {
22 | return str;
23 | }
24 |
25 | str = remove_http_protocol(str);
26 | str = remove_trailing_slash(str);
27 | str = remove_leading_www(str);
28 |
29 | return str;
30 | }
31 |
32 | function remove_trailing_slash(str) {
33 | return str.replace(/\/$/,'');
34 | }
35 |
36 | function remove_leading_www(str) {
37 | return str.replace(/^www\./,'');
38 | }
39 |
40 | function remove_http_protocol(str) {
41 | if( ! includes_protocol(str) ) {
42 | return str;
43 | }
44 | return str.replace(URL_PROTOCOL_REGEX,'');
45 | }
46 |
47 | function ensure_protocol_existence(str) {
48 | if( str && ! includes_protocol(str) ) {
49 | return 'http://'+str;
50 | }
51 | return str;
52 | }
53 |
--------------------------------------------------------------------------------
/client/src/js/util/npm_package_name_validation.js:
--------------------------------------------------------------------------------
1 | const is_npm_package_name_valid__text = '`https://npmjs.com/package/npm-package-name` is expected with `npm-package-name` being composed of `a-z`, `A-Z`, `0-9`, `-`, `.`, `_` and with the first and last character being `a-z`, `A-Z`, or `0-9` (a scoped package should start with `@` and contain a `/`)';
2 |
3 | const EXCEPTION_WHITELIST = [
4 | 'ng2-clockTST',
5 | 'NG2TableView',
6 | ];
7 |
8 | module.exports = {
9 | is_npm_package_name_valid,
10 | is_npm_package_name_valid__text,
11 | };
12 |
13 | function is_npm_package_name_valid(npm_package_name) {
14 |
15 | if( EXCEPTION_WHITELIST.includes(npm_package_name) ) {
16 | return true;
17 | }
18 |
19 | const split = npm_package_name.split('/');
20 |
21 | if( split.length > 2 ) {
22 | return false;
23 | }
24 |
25 | if( split.length===2 ) {
26 | let scope = split[0];
27 | if( ! scope.startsWith('@') ) {
28 | return false;
29 | }
30 | scope = scope.slice(1);
31 | if( ! is_valid(scope) ) {
32 | return false;
33 | }
34 | }
35 |
36 | const name = split.length===2 ? split[1] : split[0];
37 |
38 | if( ! is_valid(name) ) {
39 | return false;
40 | }
41 |
42 | return true;
43 |
44 | }
45 |
46 | function is_valid(str) {
47 | return (
48 | /^[a-z0-9\-\.\_]+$/.test(str) &&
49 | /^[a-z0-9]/.test(str) &&
50 | /[a-z0-9]$/.test(str)
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/client/src/js/util/pretty_print.js:
--------------------------------------------------------------------------------
1 | const assert = require('assertion-soft');
2 | const assert_soft = require('assertion-soft');
3 |
4 | module.exports = {
5 | points,
6 | date,
7 | age,
8 | };
9 |
10 | function points(n, {can_return_null, can_be_null}={}) {
11 | assert_soft(n || n===0 || n===null && can_be_null, n);
12 |
13 | if( ! n && n !== 0 ) {
14 | return can_return_null ? null : '?';
15 | }
16 |
17 | assert(Number.isInteger(n) && n >= 0);
18 |
19 | if( n < 1000 )
20 | return n.toString();
21 |
22 | return Math.floor(n / 1000) + 'k';
23 | }
24 |
25 | function date(d, {verbose=false, can_return_null, can_be_null, add_preposition=false}={}) {
26 | assert_soft(d || d===null && can_be_null, d);
27 |
28 | let ret = (() => {
29 | if( ! d ) {
30 | return can_return_null ? null : '?';
31 | }
32 | d = new Date(d);
33 | assert(d && !isNaN(d));
34 |
35 | if( verbose ) {
36 | return new Date(d).toLocaleDateString();
37 | }
38 |
39 | let month = d.getMonth()+1;
40 | if( month < 10 ) month = '0'+month;
41 |
42 | const year = d.getFullYear();
43 |
44 | const MILISECONDS_IN_A_MONTH = 1000*60*60*24*30.5;
45 | const LIMIT = 12*1.5*MILISECONDS_IN_A_MONTH
46 | if( new Date() - d > LIMIT ) {
47 | return year.toString();
48 | }
49 |
50 | return month + '-' + year;
51 | })();
52 |
53 | if( add_preposition && ret ) {
54 | ret = ' '+(ret.length===4?'in':'on')+' '+ret;
55 | }
56 |
57 | return ret;
58 | }
59 |
60 | function age(d, {verbose, can_return_null, can_be_null, month_approximate}={}) {
61 | assert_soft(d || d===null && can_be_null, d);
62 |
63 | if( ! d ) {
64 | return can_return_null ? null : '?';
65 | }
66 |
67 | d = new Date(d);
68 | if( ! assert_soft(d && !isNaN(d)) ) return;
69 |
70 | const minutes = (new Date() - d) / (1000*60) | 0;
71 | const hours = minutes / 60 | 0;
72 | const days = hours / 24 | 0;
73 | const months = days / 30.5 | 0
74 | const years = months / 12 | 0;
75 |
76 | if( month_approximate ) {
77 | if( months < 1 ) {
78 | return '< '+ conjugate(1, 'month');
79 | }
80 | }
81 |
82 | if( minutes < 60 )
83 | return conjugate(minutes, 'minute');
84 |
85 | if( hours < 24 )
86 | return conjugate(hours, 'hour');
87 |
88 | if( days < 31 )
89 | return conjugate(days, 'day');
90 |
91 | if( months < 12 )
92 | return conjugate(months, 'month');
93 |
94 | return conjugate(years, 'year');
95 |
96 | function conjugate(amount, unit) {
97 | const UNITES = verbose ? {
98 | 'minute': 'minute',
99 | 'hour': 'hour',
100 | 'day': 'day',
101 | 'month': 'month',
102 | 'year': 'year',
103 | } : {
104 | 'minute': 'mn',
105 | 'hour': 'h',
106 | 'day': 'd',
107 | 'month': 'm',
108 | 'year': 'y',
109 | };
110 |
111 | return !verbose ? amount+UNITES[unit] : amount+' '+UNITES[unit]+(amount===1?'':'s');
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/client/src/js/util/route_spec.js:
--------------------------------------------------------------------------------
1 | import crossroads from 'crossroads';
2 | import assert_hard from 'assertion-soft/hard';
3 | import assert_soft from 'assertion-soft';
4 |
5 | export default function route_spec(param) {
6 | assert_hard(param.constructor===Object, param);
7 | assert_hard(param.path_is_matching.constructor===Function, param);
8 | assert_hard(param.interpolate_path.constructor===Function, param);
9 | assert_hard(param.get_route_pattern.constructor===Function, param);
10 | assert_hard(param.get_route_params.constructor===Function, param);
11 |
12 | return param;
13 | };
14 |
15 | route_spec.from_crossroads_spec = function(route_string) {
16 | assert_soft(route_string && route_string.constructor===String, route_string);
17 | const crossroad_route = crossroads.addRoute(route_string);
18 | return (
19 | route_spec({
20 | path_is_matching: ({pathname}) => {
21 | assert_soft(pathname && pathname.constructor===String || pathname==='', pathname);
22 | return crossroad_route.match(pathname);
23 | },
24 | interpolate_path: args => crossroad_route.interpolate(args),
25 | get_route_pattern: () => crossroad_route._pattern,
26 | get_route_params: ({pathname}) => {
27 | assert_soft(pathname && pathname.constructor===String || pathname==='', pathname);
28 | return crossroad_route._getParamsObject(pathname);
29 | },
30 | })
31 | );
32 | };
33 |
--------------------------------------------------------------------------------
/client/src/js/util/server_uri.js:
--------------------------------------------------------------------------------
1 | export const SERVER_URI =
2 | (() => {
3 | if( typeof window === 'undefined' ) {
4 | return 'http://localhost:8081';
5 | // assert( typeof global !== 'undefined' && global.server_uri);
6 | // return global.server_uri;
7 | }
8 | if( window.location.hostname !== 'localhost' )
9 | return window.location.origin;
10 | return window.location.protocol + '//' + window.location.hostname + ':8081';
11 | })();
12 |
--------------------------------------------------------------------------------
/dev.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | sudo ls > /dev/null && # early prompt for sudo password
4 |
5 | if [[ $(whoami) == $(sudo whoami) ]] ; then
6 | echo "you shouldn't run me with root"
7 | exit 1
8 | fi
9 |
10 | (cd server/ && npm run dev) &&
11 |
12 | (cd client/ && tmux new-session -A -s hotreload "npm run start")
13 |
--------------------------------------------------------------------------------
/production.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | sudo ls > /dev/null && # early prompt for sudo password
4 |
5 | if [[ $(whoami) == $(sudo whoami) ]] ; then
6 | echo "you shouldn't run me with root"
7 | exit 1
8 | fi
9 |
10 | # git pull &&
11 |
12 | (cd server/ && yarn) &&
13 | (cd client/ && yarn) &&
14 |
15 | (cd client/ && npm run build) &&
16 | (cd server/ && npm run up)
17 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | Source code of [devarchy](http://devarchy.com/).
2 |
--------------------------------------------------------------------------------
/server/.logs/placeholder:
--------------------------------------------------------------------------------
1 | placeholder to commit the .logs directory
2 |
--------------------------------------------------------------------------------
/server/database/connection.js:
--------------------------------------------------------------------------------
1 | const env = require('../util/env');
2 |
3 | // corresponds to; `psql postgresql://user:password@host:port/database`
4 | module.exports = {
5 | host : 'localhost',
6 | port : '5432',
7 | user : 'postgres',
8 | password : env.POSTGRES_PASSWORD,
9 | database : 'devarchy',
10 | charset : 'UTF8_GENERAL_CI'
11 | };
12 |
--------------------------------------------------------------------------------
/server/database/index.js:
--------------------------------------------------------------------------------
1 | const ThingDB = require('./thingdb');
2 | const connection = require('./connection');
3 | const schema = require('./schema');
4 |
5 | const Thing = new ThingDB({
6 | connection,
7 | schema,
8 | });
9 |
10 | module.exports = Thing;
11 |
--------------------------------------------------------------------------------
/server/database/interactive.js:
--------------------------------------------------------------------------------
1 | r = require("repl").start("Thing> ");
2 | r.context.Thing = require('./');
3 | r.context.Promise_serial = require('promise-serial');
4 |
--------------------------------------------------------------------------------
/server/database/thingdb/db_interface/index.js:
--------------------------------------------------------------------------------
1 | const {save_event, save_thing} = require('./save');
2 | const {load_things, load_events, load_view} = require('./load');
3 | const table = require('./table');
4 | const create_transaction = require('./create_transaction');
5 |
6 | Object.assign(module.exports, {
7 | save_event,
8 | save_thing,
9 | load_things,
10 | load_events,
11 | load_view,
12 | table,
13 | create_transaction,
14 | });
15 |
--------------------------------------------------------------------------------
/server/database/thingdb/db_interface/load/events.js:
--------------------------------------------------------------------------------
1 | const util = require('./util');
2 |
3 |
4 | module.exports = function(){
5 | return util.retrieve_by_filter.apply(null, ['thing_event'].concat([].slice.call(arguments)));
6 | };
7 |
--------------------------------------------------------------------------------
/server/database/thingdb/db_interface/load/index.js:
--------------------------------------------------------------------------------
1 | const load_view = require('./view');
2 | const load_events = require('./events');
3 | const load_things = require('./things');
4 |
5 | Object.assign(module.exports, {
6 | load_view,
7 | load_events,
8 | load_things,
9 | });
10 |
--------------------------------------------------------------------------------
/server/database/thingdb/db_interface/load/things.js:
--------------------------------------------------------------------------------
1 | const assert = require('assert');
2 | const util = require('./util');
3 |
4 |
5 | module.exports = function(props){
6 | assert( props && Object.keys(props).length>=0 && props.length===undefined );
7 | return util.retrieve_by_filter.apply(null, ['thing_aggregate'].concat([].slice.call(arguments)));
8 | };
9 |
--------------------------------------------------------------------------------
/server/database/thingdb/db_interface/load/view.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | const Promise = require('bluebird');
3 | Promise.longStackTraces();
4 | const assert = require('better-assert');
5 | const load_things = require('./things');
6 | const util = require('./util');
7 |
8 |
9 | module.exports = load_view;
10 |
11 | function load_view(things_props, {Thing, db_handle, transaction, show_removed_things=false}={}) {
12 | assert( things_props );
13 | assert( things_props.constructor === Array )
14 | assert( things_props.length !== 0 );
15 | assert( things_props.every(thing_props => thing_props.constructor === Object) );
16 | assert( transaction === undefined || (transaction||{}).rid );
17 | assert( [true, false].includes(show_removed_things) );
18 |
19 | var request = db_handle('thing_aggregate');
20 |
21 | if( transaction ) {
22 | request = request.transacting(transaction);
23 | }
24 |
25 | request = request.select('*');
26 |
27 | if( !show_removed_things ) {
28 | request =
29 | request
30 | .where({is_removed: false});
31 | }
32 |
33 | request =
34 | request
35 | .where(db_handle.raw('id_thing::text'), 'in', function(){
36 |
37 | let sub_request = this.table('thing_aggregate');
38 |
39 | if( transaction ) {
40 | sub_request = sub_request.transacting(transaction);
41 | }
42 |
43 | sub_request =
44 | sub_request
45 | .select(db_handle.raw('unnest(views)'))
46 |
47 | if( !show_removed_things ) {
48 | sub_request =
49 | sub_request
50 | .where({is_removed: false});
51 | }
52 |
53 | things_props.forEach(thing_props => {
54 | sub_request =
55 | sub_request
56 | .where('views', '&&', db_handle.raw(
57 | load_things(thing_props, {result_fields: ['id'], Thing, db_handle, transaction, return_raw_request: true})
58 | ).wrap('array(',')::text[]'))
59 | });
60 |
61 | });
62 |
63 | Thing.debug.log.log(request.toString(), 'Transaction: '+(transaction||{}).rid);
64 |
65 | return Promise.resolve(
66 | request
67 | )
68 | .then(things =>
69 | things.map(props_from_database => util.map_props.to_thing(props_from_database))
70 | );
71 | }
72 |
73 |
74 |
--------------------------------------------------------------------------------
/server/database/thingdb/debug/get_things_sync.js:
--------------------------------------------------------------------------------
1 | const deasync = require('deasync');
2 |
3 | module.exports = function(Thing){
4 | return deasync(function(cb){
5 | Thing.database.load.things({})
6 | .then(things => {
7 | cb(null, things);
8 | });
9 | })();
10 | };
11 |
--------------------------------------------------------------------------------
/server/database/thingdb/debug/index.js:
--------------------------------------------------------------------------------
1 | const log = require('./log');
2 | const get_things_sync = require('./get_things_sync');
3 |
4 | module.exports = {
5 | log,
6 | get_things_sync,
7 | };
8 |
--------------------------------------------------------------------------------
/server/database/thingdb/debug/log.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const debug = module.exports = {
4 | buffer: {
5 | size: 100,
6 | flush,
7 | clear,
8 | },
9 | log,
10 | };
11 |
12 | debug._logs = [];
13 |
14 | function flush() {
15 | console.log('\n');
16 | debug._logs.forEach(log => {
17 | console.log('['+log.time.toLocaleTimeString()+'] Debug Info');
18 | //console.log.apply(console, log.messages);
19 | console.log([].slice.call(log.messages).join('\n'));
20 | console.log('\n');
21 | });
22 | clear();
23 | }
24 |
25 | function clear() {
26 | debug._logs = [];
27 | }
28 |
29 | function log() {
30 | debug._logs.push({
31 | time: new Date(),
32 | messages: arguments,
33 | });
34 |
35 | debug._logs = debug._logs.slice(-debug.buffer.size);
36 | }
37 |
--------------------------------------------------------------------------------
/server/database/thingdb/interpolate/apply_defaults.js:
--------------------------------------------------------------------------------
1 | const assert_soft = require('assert');
2 | const Promise = require('bluebird'); Promise.longStackTraces();
3 |
4 |
5 | module.exports = apply_defaults;
6 |
7 |
8 | function apply_defaults(thing, {schema__props__ordered}) {
9 | schema__props__ordered
10 | .forEach(({property, default_value}) => {
11 | assert_soft( property );
12 | assert_soft( default_value===undefined || default_value!==null && [Boolean, Number, String].includes(default_value.constructor) );
13 |
14 | if( default_value === undefined ) {
15 | return;
16 | }
17 |
18 | assert_soft( thing[property]!==null );
19 | if( thing[property] !== undefined ) {
20 | return;
21 | }
22 |
23 | thing[property] = default_value;
24 | });
25 | }
26 |
--------------------------------------------------------------------------------
/server/database/thingdb/interpolate/apply_side_effects.js:
--------------------------------------------------------------------------------
1 | const assert = require('assert');
2 | const assert_soft = require('assertion-soft');
3 | const Promise = require('bluebird'); Promise.longStackTraces();
4 | const Promise_serial = require('promise-serial');
5 |
6 |
7 | module.exports = apply_side_effects;
8 |
9 |
10 | function apply_side_effects(thing, {Thing, schema__options, transaction, schema__args, is_within_transaction}) {
11 | assert( Thing );
12 | assert( Thing.database );
13 | assert(schema__args && schema__args.constructor===Object);
14 | assert( [true, false].includes(is_within_transaction) );
15 |
16 | const se_args = {Thing, schema__args};
17 |
18 | assert_soft(!!is_within_transaction===!!transaction);
19 | if( is_within_transaction ) {
20 | se_args.transaction = transaction;
21 | }
22 |
23 | const side_effect_promises =
24 | (schema__options.side_effects||[])
25 | .filter(se => se.apply_outside_transaction!==is_within_transaction)
26 | .map(se => se.side_effect_computation(thing, Object.assign({}, se_args)))
27 | .filter(side_effect => side_effect!==null);
28 | assert(side_effect_promises.every(side_effect => (side_effect||0).constructor === Function));
29 |
30 | // - wihtin transaction side effects are not implemented
31 | // - ain't trivial: How do you rollback something in the side effect?
32 | assert_soft(!(is_within_transaction===true && side_effect_promises.length>0));
33 |
34 | return Promise_serial(side_effect_promises);
35 | }
36 |
--------------------------------------------------------------------------------
/server/database/thingdb/interpolate/compute_values.js:
--------------------------------------------------------------------------------
1 | const assert = require('assert');
2 | const Promise = require('bluebird'); Promise.longStackTraces();
3 | const Promise_serial = require('promise-serial');
4 |
5 |
6 | module.exports = compute_values;
7 |
8 |
9 | function compute_values(thing, {Thing, transaction, only_sync, required_props_may_be_missing, schema__props__ordered, schema__args}) {
10 | assert( Thing );
11 | assert( Thing.database );
12 | assert( Thing.SchemaError );
13 |
14 | assert(!!((transaction||{}).rid) !== !!only_sync);
15 |
16 | assert(schema__args && schema__args.constructor===Object);
17 |
18 | const props_to_compute =
19 | schema__props__ordered
20 | .filter(prop_spec => {
21 | assert( prop_spec.property );
22 |
23 | if( !prop_spec.value ) {
24 | return false;
25 | }
26 | assert(prop_spec.value.constructor === Function);
27 |
28 | if( only_sync && prop_spec.is_async ) {
29 | return false;
30 | }
31 |
32 | return true;
33 | });
34 |
35 | if( only_sync ) {
36 | props_to_compute.forEach(prop_spec => {
37 | const val = compute({prop_spec});
38 | validate_returned_obj(prop_spec, val);
39 | set_value({prop_spec, val});
40 | });
41 | return;
42 | }
43 |
44 | assert(Object.keys(thing.draft).length===0);
45 |
46 | return Promise_serial(
47 | props_to_compute.map(prop_spec => () => {
48 | const promise = compute({prop_spec});
49 |
50 | validate_returned_obj(prop_spec, promise);
51 |
52 | return (
53 | Promise.resolve(promise)
54 | )
55 | .then(val =>
56 | set_value({prop_spec, val})
57 | )
58 | .then(() => {});
59 | }
60 | )
61 | );
62 |
63 | function set_value({prop_spec, val}) {
64 | validate_value({prop_spec, val});
65 | if( val !== null ) {
66 | thing[prop_spec.property] = val;
67 | }
68 | }
69 |
70 | function validate_returned_obj(prop_spec, obj) {
71 | const is_promise = (obj||{}).then;
72 | if( prop_spec.is_async && ! is_promise ) {
73 | throw new Thing.SchemaError("following schema value function is set with `is_async=="+prop_spec.is_async+"` and should return a promise\n"+prop_spec.value);
74 | }
75 | if( ! prop_spec.is_async && is_promise ) {
76 | throw new Thing.SchemaError("following schema value function is set with `is_async=="+prop_spec.is_async+"` and shouldn't return a promise\n"+prop_spec.value);
77 | }
78 |
79 | }
80 |
81 | function validate_value({prop_spec, val}) {
82 | if( val===undefined || (val||{}).constructor===Number && isNaN(val) ) {
83 | throw new Thing.SchemaError('Property `'+prop_spec.property+'` is computed to value `'+val+'`, return `null` instead');
84 | }
85 | if( prop_spec.is_required && val===null ) {
86 | throw new Thing.SchemaError('Property `'+prop_spec.property+'` is required but is computed to value `'+val+'` for thing of type `'+thing.type+'`');
87 | }
88 | }
89 |
90 | function compute({prop_spec}) {
91 | if( required_props_may_be_missing ) {
92 | try {
93 | return doit();
94 | } catch(e) {
95 | return null;
96 | }
97 | }
98 |
99 | return doit();
100 |
101 | function doit() {
102 | return prop_spec.value(Object.assign({}, thing, thing.draft), {Thing, transaction, schema__args});
103 | }
104 | }
105 | }
106 |
107 |
--------------------------------------------------------------------------------
/server/database/thingdb/interpolate/compute_views.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | const assert = require('better-assert');
3 | const validator = require('validator');
4 |
5 |
6 | module.exports = (thing, {Thing, transaction, schema__props_spec, schema__options}) => {
7 | assert( Thing );
8 | assert( Thing.database );
9 | assert( thing.id && validator.isUUID(thing.id) );
10 | assert( thing.author && validator.isUUID(thing.author) );
11 |
12 | let views = [thing.id, thing.author];
13 |
14 | return Promise.all([
15 | add_props(),
16 | add_referred(),
17 | ])
18 | .then(_ => {
19 | thing.views = views;
20 | })
21 |
22 | function add_props() {
23 | for(let prop in schema__props_spec) {
24 | let prop_spec = schema__props_spec[prop];
25 | if( prop_spec.add_to_view ) {
26 | assert( prop_spec.is_required );
27 | let prop_value = thing[prop];
28 | assert( prop_value );
29 | assert( validator.isUUID(prop_value) );
30 | views.push(prop_value);
31 | }
32 | }
33 | return Promise.resolve();
34 | }
35 |
36 | function add_referred() {
37 | return Promise.all(
38 | (schema__options.additional_views||[])
39 | .map(fct => fct(thing, {Thing, transaction}))
40 | )
41 | .then(views_addendums => {
42 | views = views.concat(
43 | views_addendums
44 | .reduce((prev, curr) => prev.concat(curr), [])
45 | .filter(view_addendum => {
46 | // the plan is to have assert not throw in production
47 | assert(!!view_addendum);
48 | return !!view_addendum;
49 | })
50 | );
51 | });
52 | }
53 | };
54 |
--------------------------------------------------------------------------------
/server/database/thingdb/interpolate/index.js:
--------------------------------------------------------------------------------
1 | const compute_values = require('./compute_values');
2 | const apply_side_effects = require('./apply_side_effects');
3 | const aggregate_events = require('./aggregate_events');
4 | const compute_views = require('./compute_views');
5 | const apply_defaults = require('./apply_defaults');
6 |
7 | module.exports = {
8 | aggregate_events,
9 | compute_views,
10 | compute_values,
11 | apply_side_effects,
12 | apply_defaults,
13 | };
14 |
--------------------------------------------------------------------------------
/server/database/thingdb/plugins/serializer/index.js:
--------------------------------------------------------------------------------
1 | const timerlog = require('timerlog');
2 | const memoize = require('../../../../util/memoize');
3 | const assert = require('assert');
4 |
5 |
6 | module.exports = ({all_plugs}) => {
7 |
8 | all_plugs.plugs_thing_collection.push(plug_serializer);
9 |
10 | return;
11 |
12 | function plug_serializer({resume_process}) {
13 | return (
14 | resume_process()
15 | .then(output => {
16 | const things_matched = output.things_matched;
17 | assert(things_matched);
18 |
19 | const things_matched__hash = (output.plugin_memory_cache||{}).things_matched__hash;
20 | assert(output.plugin_memory_cache === undefined || things_matched__hash);
21 |
22 | const plugins_info = {};
23 | for(var key in output) {
24 | if( key.startsWith('plugin_') ) {
25 | plugins_info[key] = output[key];
26 | }
27 | }
28 |
29 | output.serialize_me = (
30 | args_serialize =>
31 | serialize(Object.assign({things_matched, things_matched__hash, plugins_info}, args_serialize))
32 | );
33 |
34 | return output;
35 | })
36 | );
37 | }
38 |
39 | };
40 |
41 | function serialize({things_matched, things_matched__hash, plugins_info, include_side_props, include_plugins_info}) {
42 |
43 | const cache_key = (
44 | {
45 | things_matched__hash,
46 | plugins_info,
47 | include_side_props,
48 | include_plugins_info,
49 | }
50 | );
51 |
52 | if( ! things_matched__hash ) {
53 | return computation();
54 | }
55 |
56 | return (
57 | memoize({
58 | memory_cluster: 'serialize_plugin',
59 | cache_key,
60 | computation,
61 | }).content
62 | );
63 |
64 | function computation() {
65 | timerlog({
66 | id: 'slow_serialize_things',
67 | tag: 'slowiness_tracker',
68 | measured_time_threshold: 1000,
69 | start_timer: true,
70 | });
71 | timerlog({
72 | id: 'serialize_things',
73 | tags: ['performance', 'db_processing'],
74 | start_timer: true,
75 | });
76 |
77 | const obj = {};
78 |
79 | obj.things_matched = things_matched;
80 |
81 | if( include_plugins_info ) {
82 | Object.assign(obj, plugins_info)
83 | }
84 |
85 | const obj_str = JSON.stringify(obj);
86 |
87 | timerlog({
88 | id: 'serialize_things',
89 | end_timer: true,
90 | });
91 | timerlog({
92 | id: 'slow_serialize_things',
93 | end_timer: true,
94 | });
95 |
96 | return obj_str;
97 | }
98 |
99 | }
100 |
--------------------------------------------------------------------------------
/server/database/thingdb/schema_common.js:
--------------------------------------------------------------------------------
1 | const validator = require('validator');
2 | // schema common to all Thing types
3 |
4 | const schema = {
5 | author: {
6 | validation: {
7 | type: 'Thing.user',
8 | },
9 | is_required: true,
10 | },
11 | id: {
12 | validation: {
13 | type: String,
14 | test: v => validator.isUUID(v),
15 | },
16 | is_required: true,
17 | is_unique: true,
18 | },
19 | type: {
20 | validation: {
21 | type: String,
22 | },
23 | is_required: true,
24 | },
25 | is_removed: {
26 | validation: {
27 | type: Boolean,
28 | },
29 | is_required: true,
30 | default_value: false,
31 | },
32 | history: {
33 | validation: {
34 | type: Array,
35 | },
36 | is_not_user_generated: true,
37 | is_non_enumerable: true,
38 | },
39 | updated_at: {
40 | validation: {
41 | type: Date,
42 | },
43 | is_not_user_generated: true,
44 | },
45 | created_at: {
46 | validation: {
47 | type: Date,
48 | },
49 | is_not_user_generated: true,
50 | },
51 | computed_at: {
52 | validation: {
53 | type: Date,
54 | },
55 | is_not_user_generated: true,
56 | },
57 | views: {
58 | validation: {
59 | type: Array,
60 | },
61 | is_not_user_generated: true,
62 | is_non_enumerable: true,
63 | },
64 | subtype: {
65 | validation: {
66 | type: String,
67 | },
68 | is_not_user_generated: true,
69 | is_non_enumerable: true,
70 | },
71 | };
72 |
73 | module.exports = {
74 | thing: schema,
75 | draft: {
76 | author: schema.author,
77 | is_removed: schema.is_removed,
78 | },
79 | };
80 |
--------------------------------------------------------------------------------
/server/database/thingdb/test/devarchy/index.js:
--------------------------------------------------------------------------------
1 | require('./tests/markdown_translation');
2 | require('./tests/frontend');
3 | require('./tests/schema');
4 |
--------------------------------------------------------------------------------
/server/database/thingdb/test/devarchy/playground.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | require('mocha');
3 | const assert = require('better-assert');
4 | const Promise = require('bluebird'); Promise.longStackTraces();
5 | const Thing = require('./thing')
6 | const promise = require('../test-promise')(Thing);
7 | const setup = require('./setup')(Thing);
8 | const population = require('./population');
9 |
10 |
11 | describe('ThingDB', () => {
12 |
13 | before(setup);
14 |
15 | before(population.create);
16 |
17 | promise.it("can remove not required properties", () =>
18 | /*
19 | new Thing({
20 | draft: {
21 | author: population.user.id,
22 | }
23 | }).draft.save()
24 | */
25 | );
26 |
27 | });
28 |
29 |
--------------------------------------------------------------------------------
/server/database/thingdb/test/devarchy/population.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | const assert = require('better-assert');
3 | const Thing = require('./thing');
4 |
5 |
6 | const population = module.exports = {
7 | user: null,
8 | create,
9 | };
10 |
11 | let promise;
12 | function create() {
13 |
14 | if( !promise ) { promise = create_promise(); }
15 |
16 | return promise;
17 |
18 | function create_promise() {
19 | return Thing.database.load.things({type: 'user', github_login: 'brillout'})
20 | .then(things => {
21 | if( things.length === 1 ) {
22 | population.user = things[0];
23 | return;
24 | }
25 | assert( things.length === 0 );
26 | const user = new Thing({
27 | type: 'user',
28 | draft: {
29 | github_login: 'brillout',
30 | },
31 | });
32 | user.generate_id();
33 | assert(user.id);
34 | user.draft.author = user.id;
35 | population.user = user;
36 | return user.draft.save();
37 | })
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/server/database/thingdb/test/devarchy/setup.js:
--------------------------------------------------------------------------------
1 | require('../../../../../.env');
2 | require('mocha');
3 | const assert = require('assert');
4 | require('timerlog')({disable_all: true});
5 |
6 | module.exports = setup;
7 |
8 | const already_run = new WeakMap();
9 |
10 | function setup(Thing) {
11 | assert(Thing);
12 |
13 | if( already_run.has(Thing) ) {
14 | return;
15 | }
16 | already_run.set(Thing);
17 |
18 | const promise = require('../test-promise')(Thing);
19 |
20 | describe('ThingDB management', () => {
21 |
22 | promise.it('can delete and re-create database schema', () =>
23 | Thing.database.management.purge_everything()
24 | )
25 | });
26 | }
27 |
--------------------------------------------------------------------------------
/server/database/thingdb/test/devarchy/thing.js:
--------------------------------------------------------------------------------
1 | module.exports = require('../thing')({
2 | database_name: 'devarchy__tests__devarchy',
3 | schema: require('../../../schema.js'),
4 | });
5 |
--------------------------------------------------------------------------------
/server/database/thingdb/test/features/index.js:
--------------------------------------------------------------------------------
1 | require('./tests/core');
2 | require('./tests/validation');
3 | require('./tests/uniqueness');
4 | require('./tests/computed_props');
5 | require('./tests/upsert');
6 | require('./tests/subtypes');
7 | require('./tests/cascading_save');
8 | require('./tests/views');
9 | require('./tests/relations');
10 | require('./tests/loading');
11 | require('./tests/details');
12 | require('./tests/misc');
13 | require('./tests/concurrency');
14 | require('./tests/migrations');
15 |
--------------------------------------------------------------------------------
/server/database/thingdb/test/features/setup.js:
--------------------------------------------------------------------------------
1 | require('../../../../../.env');
2 | require('mocha');
3 | const assert = require('assert');
4 | require('timerlog')({disable_all: true});
5 |
6 | module.exports = setup;
7 |
8 | function setup(Thing) {
9 | assert(Thing);
10 |
11 | return (
12 | function() {
13 | this.timeout(10*1000);
14 | return (
15 | Thing.database.management.purge_everything()
16 | );
17 | }
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/server/database/thingdb/test/features/tests/computed_props.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | require('mocha');
3 | const assert_better = require('better-assert');
4 | const assert = require('assert');
5 | const Thing = require('../thing').custom_schema(schema(), 'computed_props');
6 | const setup = require('../setup')(Thing);
7 | const promise = require('../../test-promise')(Thing);
8 |
9 | let author;
10 |
11 | describe("ThingDB - computed properties", () => {
12 | before(setup);
13 |
14 | before(create_user);
15 |
16 | promise.it("supports synchronous computed properties", () => {
17 | return (
18 | new Thing({
19 | type: 'resource',
20 | draft: {
21 | url: 'http://brillout.com',
22 | author,
23 | },
24 | }).draft.save()
25 | .then(([resource]) => {
26 | assert_better(resource.url_normalized === 'brillout.com');
27 | })
28 | );
29 | });
30 |
31 | promise.it("supports asynchronous computed properties", () => {
32 | const url = 'http://brillout.com';
33 | return (
34 | new Thing({
35 | type: 'resource',
36 | url,
37 | author,
38 | draft: {},
39 | }).draft.save()
40 | .then(([resource]) => {
41 | assert_better(resource.url_information.fake_info === url+'/pathi');
42 | })
43 | );
44 | });
45 | });
46 |
47 | function create_user() {
48 | return (
49 | new Thing({
50 | type: 'user',
51 | name: 'fake user',
52 | }).draft.save()
53 | .then(([user]) => {
54 | author = user.id;
55 | })
56 | );
57 | }
58 |
59 | function schema() {
60 | return {
61 | user: {
62 | name: {
63 | validation: {
64 | type: String,
65 | },
66 | is_unique: true,
67 | },
68 | },
69 | resource: {
70 | url: {
71 | validation: {
72 | type: String,
73 | },
74 | is_unique: true,
75 | },
76 | url_normalized: {
77 | validation: {
78 | type: String,
79 | },
80 | value: (thing_self, args) => {
81 | assert_args(thing_self, args);
82 | return thing_self.url.replace(/https?:\/\//,'');
83 | },
84 | },
85 | url_information: {
86 | validation: {
87 | type: Object,
88 | },
89 | is_async: true,
90 | value: (thing_self, args) => {
91 | assert_args(thing_self, args);
92 | return (
93 | new Promise(resolve => {
94 | setTimeout(() => {
95 | resolve({fake_info: thing_self.url+'/pathi'});
96 | }, 300);
97 | })
98 | );
99 | },
100 | }
101 | },
102 | };
103 |
104 | function assert_args(thing_self, args) {
105 | assert_better(thing_self);
106 | assert_better(thing_self.constructor === Object);
107 | assert_better(thing_self.id);
108 | assert_better(args.Thing);
109 | assert_better(args.Thing===Thing);
110 | assert_better(args.transaction);
111 | assert_better(args.transaction.rid);
112 | assert((args.schema__args||1).constructor===Object, JSON.stringify(args));
113 | assert_better(Object.keys(args).length===3);
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/server/database/thingdb/test/features/tests/details.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | require('mocha');
3 | const assert = require('better-assert');
4 | const Thing = require('../thing').Thing;
5 | const setup = require('../setup')(Thing);
6 | const promise = require('../../test-promise')(Thing);
7 | const population = require('../population');
8 |
9 |
10 | describe("ThingDB details", () => {
11 | before(setup);
12 |
13 | before(population.create);
14 |
15 | /* TODO
16 | promise.it('properly handle properties set to undefined', () => {
17 | Thing.database.load.view([{
18 | type: 'user',
19 | name: population.user.name,
20 | bio: undefined,
21 | }])
22 | });
23 | */
24 |
25 | });
26 |
--------------------------------------------------------------------------------
/server/database/thingdb/test/features/tests/loading.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | require('mocha');
3 | const assert = require('better-assert');
4 | const Thing = require('../thing').Thing;
5 | const setup = require('../setup')(Thing);
6 | const promise = require('../../test-promise')(Thing);
7 | const population = require('../population');
8 |
9 |
10 | describe("ThingDB - things loading", () => {
11 | before(setup);
12 |
13 | before(population.create);
14 |
15 | promise.it('filter support arbitrary knex operators', () => {
16 | const resource = population.resources[0];
17 | const resource2 = population.resources[1];
18 | assert(resource.created_at && resource2.created_at && resource.created_at !== resource2.created_at);
19 | const resource_latest = resource2.created_at > resource.created_at ? resource2 : resource;
20 | const resource_first = resource_latest === resource2 ? resource : resource2;
21 | return (
22 | Thing.database.load.things({
23 | type: 'resource',
24 | created_at: {
25 | operator: '<',
26 | value: resource_latest.created_at,
27 | },
28 | })
29 | )
30 | .then(things => {
31 | assert( things.every(t => t.id !== resource_latest.id) );
32 | assert( things.some(t => t.id === resource_first.id) );
33 | })
34 | });
35 |
36 | });
37 |
38 |
39 |
--------------------------------------------------------------------------------
/server/database/thingdb/test/features/tests/migrations.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | require('mocha');
3 | const assert = require('better-assert');
4 | const Promise_serial = require('promise-serial');
5 | const Thing = require('../thing').Thing;
6 | const setup = require('../setup')(Thing);
7 | const promise = require('../../test-promise')(Thing);
8 | const population = require('../population');
9 |
10 |
11 | describe("ThingDB - migration", () => {
12 | before(setup);
13 |
14 | before(population.create);
15 |
16 | promise.it("can recompute things", () => (
17 | Thing.database.load.all_things()
18 | .then(things =>
19 | Promise_serial(
20 | things.map(t => () => t.recompute())
21 | )
22 | )
23 | ));
24 |
25 | });
26 |
--------------------------------------------------------------------------------
/server/database/thingdb/test/features/tests/misc.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | require('mocha');
3 | const assert = require('better-assert');
4 | const Thing = require('../thing').Thing;
5 | const setup = require('../setup')(Thing);
6 | const promise = require('../../test-promise')(Thing);
7 | const population = require('../population');
8 |
9 | describe("ThingDB - misc", () => {
10 | before(setup);
11 |
12 | before(population.create_all);
13 |
14 | promise.it("can recompute things from events", () => {
15 | const value = new Date(new Date()-1);
16 | return (
17 | Thing.database.load.things({
18 | type: 'resource',
19 | computed_at: {
20 | operator: '<=',
21 | value,
22 | },
23 | })
24 | .then(things => {
25 | assert(things.length>0);
26 | })
27 | )
28 | .then(() =>
29 | Thing.recompute_all({
30 | type: 'resource',
31 | computed_at: {
32 | operator: '<=',
33 | value,
34 | },
35 | })
36 | )
37 | .then( () =>
38 | Thing.database.load.things({
39 | type: 'resource',
40 | computed_at: {
41 | operator: '<=',
42 | value,
43 | },
44 | })
45 | .then(things => {
46 | assert(things.length===0);
47 | })
48 | );
49 | });
50 |
51 | promise.it_validates_if("saving thing that is already existing after purging database (unique constraints are not lost after deleting database)",
52 | () => (
53 | (
54 | Thing.database.management.purge_everything()
55 | )
56 | .then(() =>
57 | population.create_all()
58 | )
59 | .then(() =>
60 | new Thing({
61 | type: 'tag',
62 | draft: {
63 | name: population.tags[0].name,
64 | author: population.user.id,
65 | },
66 | })
67 | .draft.save()
68 | )
69 | .then(() => {
70 | return (
71 | Thing.database.load.things({
72 | type: 'tag',
73 | name: population.tags[0].name,
74 | })
75 | );
76 | })
77 | .then(things => {
78 | assert(things.length < 2);
79 | assert(things.length === 1);
80 | })
81 | ),
82 | { reason: /Thing with type `tag` and name `.*` already exists in database/}
83 | );
84 |
85 |
86 | });
87 |
--------------------------------------------------------------------------------
/server/database/thingdb/test/features/tests/uniqueness.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | require('mocha');
3 | const assert = require('better-assert');
4 | const Thing = require('../thing').Thing;
5 | const setup = require('../setup')(Thing);
6 | const promise = require('../../test-promise')(Thing);
7 | const population = require('../population');
8 |
9 |
10 | describe("ThingDB's uniqueness handling", () => {
11 | before(setup);
12 |
13 | before(population.create_all);
14 |
15 | before(done => {
16 | new Thing({
17 | type: 'resource',
18 | draft: {
19 | author: population.user.id,
20 | name: 'Romuald',
21 | },
22 | })
23 | .draft.save()
24 | .then(() => done())
25 | });
26 |
27 | promise.it_validates_if("saving thing that is already existing",
28 | () => (
29 | (
30 | new Thing({
31 | type: 'tag',
32 | draft: {
33 | name: population.tags[0].name,
34 | author: population.user.id,
35 | },
36 | })
37 | .draft.save()
38 | )
39 | .then(() => {
40 | return (
41 | Thing.database.load.things({
42 | type: 'tag',
43 | name: population.tags[0].name,
44 | })
45 | );
46 | })
47 | .then(things => {
48 | assert(things.length < 2);
49 | assert(things.length === 1);
50 | })
51 | ),
52 | { reason: /Thing with type `tag` and name `.*` already exists in database/}
53 | );
54 |
55 | promise.it_validates_if("saving thing with already existing computed value with `is_unique===true`",
56 | () => {
57 | return (
58 | (
59 | Thing.database.load.things({
60 | type: 'resource',
61 | name_normalized: 'romuald',
62 | })
63 | )
64 | .then(things => {
65 | assert(things.length===1);
66 | assert(things[0].name_normalized==='romuald');
67 | assert(things[0].name==='Romuald');
68 | })
69 | .then(() =>
70 | new Thing({
71 | type: 'resource',
72 | draft: {
73 | author: population.user.id,
74 | name: '@romuald',
75 | },
76 | }).draft.save()
77 | )
78 | .then(() => {
79 | return (
80 | Thing.database.load.things({
81 | type: 'resource',
82 | name_normalized: 'romuald',
83 | })
84 | );
85 | })
86 | .then(things => {
87 | assert(things.length < 2);
88 | assert(things.length === 1);
89 | })
90 | );
91 | },
92 | { reason: "Thing with type `resource` and name_normalized `romuald` already exists in database"}
93 | );
94 | });
95 |
96 |
--------------------------------------------------------------------------------
/server/database/thingdb/test/features/thing.js:
--------------------------------------------------------------------------------
1 | const assert = require('assert');
2 | module.exports = {
3 | Thing: init_Thing(require('./schema.js'), null),
4 | custom_schema: init_Thing,
5 | };
6 |
7 | function init_Thing(schema, name) {
8 | let database_name = 'devarchy__tests__features';
9 | assert(schema.constructor===Object);
10 | assert(name===null || (name||{}).constructor === String);
11 | if( name ) {
12 | database_name += '__'+name;
13 | }
14 | return (
15 | require('../thing')({
16 | database_name,
17 | schema,
18 | })
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/server/database/thingdb/test/index.js:
--------------------------------------------------------------------------------
1 | require('./features');
2 | require('./devarchy');
3 |
--------------------------------------------------------------------------------
/server/database/thingdb/test/mocha.opts:
--------------------------------------------------------------------------------
1 | --bail
2 |
--------------------------------------------------------------------------------
/server/database/thingdb/test/thing.js:
--------------------------------------------------------------------------------
1 | const assert = require('assert');
2 | const ThingDB = require('../index.js');
3 | const conn = require('../../connection.js');
4 |
5 | module.exports = ({database_name, schema}) => {
6 | assert(database_name, schema);
7 | const connection = Object.assign({}, conn, {database: database_name});
8 | const Thing = new ThingDB({
9 | connection,
10 | schema,
11 | http_max_delay: 1,
12 | dont_throw_on_connection_errors: true,
13 | schema__args: {
14 | is_a_test_run: true,
15 | },
16 | });
17 | assert(Thing.database);
18 | return Thing;
19 | };
20 |
--------------------------------------------------------------------------------
/server/database/thingdb/util/deep-assign.js:
--------------------------------------------------------------------------------
1 | // MOD of https://github.com/sindresorhus/deep-assign
2 |
3 |
4 | 'use strict';
5 | var hasOwnProperty = Object.prototype.hasOwnProperty;
6 | var propIsEnumerable = Object.prototype.propertyIsEnumerable;
7 |
8 | function toObject(val) {
9 | if (val === null || val === undefined) {
10 | throw new TypeError('Sources cannot be null or undefined');
11 | }
12 |
13 | return Object(val);
14 | }
15 |
16 | function assignKey(to, from, key) {
17 | var val = from[key];
18 |
19 | if (val === undefined || val === null) {
20 | return;
21 | }
22 |
23 | if (hasOwnProperty.call(to, key)) {
24 | if (to[key] === undefined || to[key] === null) {
25 | throw new TypeError('Cannot convert undefined or null to object (' + key + ')');
26 | }
27 | }
28 |
29 | // MOD
30 | if (val===undefined || val===undefined || val.constructor!==Object) {
31 | to[key] = val;
32 | } else {
33 | to[key] = assign(Object(to[key]), from[key]);
34 | }
35 | }
36 |
37 | function assign(to, from) {
38 | if (to === from) {
39 | return to;
40 | }
41 |
42 | from = Object(from);
43 |
44 | for (var key in from) {
45 | if (hasOwnProperty.call(from, key)) {
46 | assignKey(to, from, key);
47 | }
48 | }
49 |
50 | if (Object.getOwnPropertySymbols) {
51 | var symbols = Object.getOwnPropertySymbols(from);
52 |
53 | for (var i = 0; i < symbols.length; i++) {
54 | if (propIsEnumerable.call(from, symbols[i])) {
55 | assignKey(to, from, symbols[i]);
56 | }
57 | }
58 | }
59 |
60 | return to;
61 | }
62 |
63 | module.exports = function deepAssign(target) {
64 | target = toObject(target);
65 |
66 | for (var s = 1; s < arguments.length; s++) {
67 | assign(target, arguments[s]);
68 | }
69 |
70 | return target;
71 | };
72 |
--------------------------------------------------------------------------------
/server/database/thingdb/util/deep-equal.js:
--------------------------------------------------------------------------------
1 | const assert = require('assert');
2 |
3 | module.exports = deep_equal;
4 |
5 | /*
6 | const assert = require('assert');
7 |
8 | function deep_equal(a, b, keys_to_ignore=[]) {
9 | assert([a, b].every(v => [null, undefined].includes(v) || [Object, Array, Number, String, Boolean].includes(v.constructor)), a);
10 | if( [a, b].every(o => (o||0).constructor === Object) ) {
11 | if( ! same_content(Object.keys(a), Object.keys(b), keys_to_ignore) )
12 | return false;
13 | for(let i in b) if( ! keys_to_ignore.includes(i) && ! deep_equal(a[i], b[i]) ) return false;
14 | return true;
15 | }
16 | return a === b;
17 |
18 | function same_content(arr1, arr2, elements_to_ignore) {
19 | return (
20 | arr1.every(e => elements_to_ignore.includes(e) || arr2.includes(e)) &&
21 | arr2.every(e => elements_to_ignore.includes(e) || arr1.includes(e))
22 | );
23 | }
24 | }
25 | */
26 |
27 | function deep_equal(a, b, keys_to_ignore=[]) {
28 | assert([a, b].every(v => is_primitive(v) || is_iterable(v)), a);
29 |
30 | if( [a, b].every(o => is_iterable(o)) ) {
31 | return (
32 | []
33 | .concat(Object.keys(a), Object.keys(b))
34 | .filter(key => !keys_to_ignore.includes(key))
35 | .every(key => deep_equal(a[key], b[key]))
36 | );
37 | }
38 |
39 | if( [a, b].some(v => v&&v.constructor===Date) ) {
40 | const a_epoch = new Date(a).getTime();
41 | const b_epoch = new Date(b).getTime();
42 | if( isNaN(a_epoch) || isNaN(b_epoch) ) {
43 | return false;
44 | }
45 | return a_epoch === b_epoch;
46 | }
47 |
48 | return a === b;
49 |
50 |
51 | function is_primitive(v) {
52 | return [null, undefined].includes(v) || [Date, Number, String, Boolean].includes(v.constructor);
53 | }
54 |
55 | function is_iterable(v) {
56 | return v instanceof Object;
57 | }
58 | }
59 |
60 |
--------------------------------------------------------------------------------
/server/database/thingdb/util/obj-path.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | get_val,
3 | set_val,
4 | as_array,
5 | };
6 |
7 | function get_val(obj, path) {
8 | let val = obj;
9 | as_array(path).forEach(key => val = (val||{})[key]);
10 | return val;
11 | }
12 |
13 | function set_val(obj, val, path) {
14 | let obj_child = obj;
15 | const dirs = as_array(path).slice(0, -1);
16 | const key = as_array(path).slice(-1);
17 | dirs.forEach(dir => {
18 | if( obj_child[dir]!==undefined && !( obj_child[dir] instanceof Object ) ) {
19 | throw new Error('obj_path: '+dir+' is already set to a non-object-instance: '+obj_child[dir]);
20 | }
21 | obj_child[dir] = obj_child[dir] || {};
22 | obj_child = obj_child[dir];
23 | })
24 | obj_child[key] = val;
25 | }
26 |
27 | function as_array(path) {
28 | return path.split('.');
29 | }
30 |
--------------------------------------------------------------------------------
/server/database/thingdb/validate/index.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./thing');
2 | module.exports.load_props = require('./load_props');
3 |
--------------------------------------------------------------------------------
/server/database/thingdb/validate/load_props.js:
--------------------------------------------------------------------------------
1 | const validator = require('validator');
2 |
3 | module.exports = (props, {Thing, type_is_optional}) => {
4 | const prefix = "things loading properties criteria";
5 | if( Object.keys(props) === 0 ) {
6 | throw new Thing.ValidationError(prefix+" are missing");
7 | }
8 | if( "id" in props ) {
9 | if( ! props.id || props.id.constructor!==String || ! validator.isUUID(props.id) ) {
10 | throw new Thing.ValidationError(prefix+"'s `id` is not a UUID: `"+props.id+"`\n"+JSON.stringify(props, null, 2));
11 | }
12 | return;
13 | }
14 | if( ! props.type && !type_is_optional ) {
15 | throw new Thing.ValidationError(prefix+" is missing `type`");
16 | return;
17 | }
18 | Object.entries(props)
19 | .forEach(([prop, val]) => {
20 | if( val===undefined ) {
21 | throw new Thing.ValidationError(prefix+" has `"+prop+"` equals to `undefined` which is forbidden");
22 | }
23 | });
24 | };
25 |
--------------------------------------------------------------------------------
/server/http/auth/providers.js:
--------------------------------------------------------------------------------
1 | module.exports = [
2 | {
3 | provider_name: 'github',
4 | scope: ['user:email'],
5 | identifier: {
6 | name: 'github_login',
7 | retriever: profile => profile.raw.login,
8 | },
9 | },
10 | {
11 | provider_name: 'twitter',
12 | config: {
13 | extendedProfile: true,
14 | getMethod: 'account/verify_credentials',
15 | getParams: {
16 | include_email: "true", // `true` -> OAuth dance fails
17 | },
18 | },
19 | identifier: {
20 | name: 'twitter_user_id',
21 | retriever: profile => profile.id,
22 | },
23 | private_info_retriever: pick.bind(null, ['email', 'location', 'description', 'url', 'followers_count', 'friends_count', 'listed_count', 'verified', 'lang','default_profile_image', 'default_profile', 'needs_phone_verification', 'created_at', 'favourites_count', 'utc_offset', 'time_zone', 'statuses_count', 'protected', ]),
24 | info_retriever: pick.bind(null, ['id', 'screen_name', 'name', 'last_name', 'profile_image_url_https']),
25 | },
26 | {
27 | provider_name: 'facebook',
28 | scope: ['email'],
29 | identifier: {
30 | name: 'facebook_user_id',
31 | retriever: profile => profile.id,
32 | },
33 | profileParams: ['id' ,'name' , 'email', 'first_name' , 'last_name', 'gender' , 'link', 'locale', 'timezone', 'updated_time', 'verified', 'is_verified'],
34 | private_info_retriever: pick.bind(null, ['email', 'gender', 'link', 'locale', 'timezone', 'updated_time', 'verified', 'is_verified']),
35 | info_retriever: pick.bind(null, ['id', 'name', 'first_name', 'last_name']),
36 | },
37 | ];
38 |
39 | function pick(keys, obj) {
40 | const ret = {};
41 | keys.forEach(k => ret[k] = obj[k]);
42 | return ret;
43 | }
44 |
--------------------------------------------------------------------------------
/server/http/client/index.js:
--------------------------------------------------------------------------------
1 | const assert_soft = require('assertion-soft');
2 | const timerlog = require('timerlog');
3 |
4 |
5 | module.exports = function(server) {
6 | server.route({
7 | method: 'GET',
8 | path: '/',
9 | handler,
10 | });
11 | server.route({
12 | method: 'GET',
13 | path: '/{param*}',
14 | handler,
15 | });
16 | };
17 |
18 |
19 | {
20 | let html;
21 | if( process.env['NODE_ENV'] === 'production' ) {
22 | html = require('./html');
23 | }
24 | function handler(request, reply) {
25 | const pathname = request.url.pathname;
26 | assert_soft(pathname);
27 |
28 | const log_id__overall = timerlog({
29 | start_timer: true,
30 | message: "HTML computed for "+pathname,
31 | tags: ["http_request", "new_visit"],
32 | });
33 |
34 | // `require('./html')` loads whole machinery to compiles client side code
35 | // -> we therefore `require('./html')` only when we need to
36 | html = require('./html');
37 |
38 | const {production_url, hostname} = (() => {
39 | const headers = request.headers;
40 |
41 | const host = headers.host;
42 | assert_soft(host, headers);
43 | assert_soft(request.info.host===host, headers, request.info.host);
44 |
45 | let hostname = (host||'').split(':')[0].toLowerCase();
46 | // assert_soft(['devarchy.com', 'localhost', '52.37.197.83', ].includes(hostname), '`hostname===`'+hostname);
47 | const expected_hosts = ['devarchy.com', 'localhost'];
48 | hostname = expected_hosts.includes(hostname) ? hostname : expected_hosts[0];
49 | // console.log(require('circular-json').stringify(Object.assign({}, request, {connection: null, server: null, _router: null, settings: null}), null, 2));
50 |
51 | const production_url = assert_soft(pathname[0]==='/') ? ('https://devarchy.com'+pathname) : null;
52 |
53 | return {production_url, hostname};
54 | })();
55 |
56 | html({pathname, hostname, production_url})
57 | .then(({html_str, redirect_to, return_status_code}) => {
58 |
59 | assert_soft(!return_status_code || return_status_code.constructor===Number, return_status_code);
60 | assert_soft(!redirect_to || return_status_code, redirect_to, return_status_code);
61 | assert_soft(!redirect_to || redirect_to.constructor===String && redirect_to[0]==='/', redirect_to);
62 |
63 | let rep = reply(html_str || undefined);
64 |
65 | if( redirect_to ) {
66 | assert_soft(redirect_to.constructor===String);
67 | rep = rep.redirect(redirect_to);
68 | }
69 |
70 | if( return_status_code ) {
71 | rep.code(return_status_code);
72 | }
73 |
74 | timerlog({
75 | id: log_id__overall,
76 | end_timer: true,
77 | });
78 | });
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/server/http/config.js:
--------------------------------------------------------------------------------
1 | const env = require('../util/env');
2 |
3 | module.exports = {
4 | host: undefined, // let hapi figure out the right IP address on the server
5 | port: 8081,
6 | protocol: 'http://',
7 | dev_client: {
8 | host: 'localhost',
9 | port: 8082,
10 | protocol: 'http://'
11 | }
12 | };
13 |
--------------------------------------------------------------------------------
/server/http/http_cache.js:
--------------------------------------------------------------------------------
1 | const interpret_response = require('./error_handler').interpret_response;
2 | const timerlog = require('timerlog');
3 | const assert_soft = require('assertion-soft');
4 | const nodejs_hash = require('../util/nodejs_hash');
5 |
6 |
7 | module.exports = http_cache;
8 |
9 |
10 | function http_cache(server) {
11 | server.ext('onPreResponse', (request, reply) => {
12 |
13 | const {is_ok, is_internal_error, is_not_found} = interpret_response(request.response);
14 | if( !is_ok || is_internal_error || is_not_found ) {
15 | reply.continue();
16 | return;
17 | }
18 |
19 | {
20 | const method = request.method;
21 | assert_soft(['get', 'post', 'options', 'head', ].includes(method), method);
22 | if( method!=='get' ) {
23 | reply.continue();
24 | return;
25 | }
26 | }
27 |
28 | if( request.response.variety === 'file' ) {
29 | // setting to one year; http://stackoverflow.com/questions/7071763/max-value-for-cache-control-header-in-http
30 | // immutable support; http://stackoverflow.com/questions/41936772/which-browsers-support-cache-control-immutable
31 | const path = request.path;
32 | const is_in_static_path = path.startsWith('/static/');
33 | assert_soft(is_in_static_path);
34 | const expected_extensions = [
35 | 'js',
36 | 'css',
37 | 'map',
38 |
39 | 'ico',
40 | 'svg',
41 | 'png',
42 |
43 | 'json',
44 | 'webapp',
45 | 'xml',
46 |
47 | 'eot',
48 | 'woff',
49 | 'woff2',
50 | 'ttf',
51 | ];
52 | const extension_is_expected = expected_extensions.includes(path.split('.').slice(-1)[0]);
53 | assert_soft(extension_is_expected, path);
54 | if( is_in_static_path && extension_is_expected ) {
55 | request.response.header('Cache-control', 'public, max-age=31536000, immutable');
56 | }
57 | } else {
58 | const src = request.response.source;
59 | assert_soft(src, request.path, request.response.source);
60 | if( src ) {
61 | timerlog({
62 | id: 'hash_http_response',
63 | tags: ['performance', 'db_processing'],
64 | start_timer: true,
65 | });
66 | request.response.etag(nodejs_hash(src));
67 | timerlog({
68 | id: 'hash_http_response',
69 | end_timer: true,
70 | });
71 | }
72 | }
73 |
74 | reply.continue();
75 |
76 | });
77 | }
78 |
79 |
--------------------------------------------------------------------------------
/server/http/index.js:
--------------------------------------------------------------------------------
1 | const hapi = require('hapi');
2 | const auth = require('./auth');
3 | const api = require('./api');
4 | const client = require('./client');
5 | const http_cache = require('./http_cache');
6 | const static_dir = require('./static_dir');
7 | const error_handler = require('./error_handler').error_handler;
8 | const config = require('./config');
9 | const log = require('./log');
10 |
11 |
12 | const server = new hapi.Server();
13 | server.connection(connection());
14 |
15 | const bootup_promise = (
16 | server
17 | .register(plugins())
18 | .then(callback)
19 | .then(() => server)
20 | );
21 |
22 |
23 | module.exports = bootup_promise;
24 |
25 | function connection() {
26 | return {
27 | host: config.host,
28 | port: config.port,
29 | routes: {
30 | cors: {
31 | origin: [
32 | [
33 | config.dev_client.protocol,
34 | config.dev_client.host,
35 | ':',
36 | config.dev_client.port
37 | ].join('')
38 | ],
39 | credentials: true
40 | }
41 | },
42 | };
43 | }
44 |
45 | function plugins() {
46 | return [
47 | require('bell'),
48 | require('inert'),
49 | require('hapi-auth-cookie'),
50 | ]
51 | }
52 |
53 | function callback(err) {
54 | if( err ) console.error('Failed to load a plugin:', err);
55 |
56 | log(server);
57 |
58 | static_dir(server);
59 |
60 | error_handler(server);
61 |
62 | http_cache(server);
63 |
64 | auth(server);
65 |
66 | api(server);
67 |
68 | client(server);
69 |
70 | return server.start();
71 | }
72 |
73 | // gloabl.server_uri = server.info.uri;
74 |
--------------------------------------------------------------------------------
/server/http/log.js:
--------------------------------------------------------------------------------
1 | const winston = require('winston');
2 | const path = require('path');
3 |
4 | const logs_dir_path = path.join(__dirname, '../.logs');
5 |
6 | const LISTENERS = [
7 | {
8 | filter: event => event.event_type === 'request-error',
9 | print: event => event.error_info,
10 | file: path.join(logs_dir_path, 'errors.txt'),
11 | },
12 | {
13 | filter: event => event.event_type === 'response',
14 | print: event => decodeURIComponent(get_prop_val(event, 'url.path')),
15 | file: path.join(logs_dir_path, 'responses.txt'),
16 | },
17 | {
18 | filter: event => event.event_type === 'response' && get_prop_val(event, 'url.path') === '/api/user',
19 | print: () => 'new visit',
20 | file: path.join(logs_dir_path, 'visits.txt'),
21 | },
22 | ];
23 |
24 |
25 | module.exports = function(server) {
26 | server.on('response', event => {
27 | event.event_type = 'response';
28 | on_event(event);
29 | });
30 |
31 | server.on('request-error', (event, err) => {
32 | event.event_type = 'request-error';
33 | event.error_info = err;
34 | on_event(event);
35 | });
36 |
37 | create_loggers();
38 | };
39 |
40 |
41 | function on_event(event) {
42 | LISTENERS.forEach(listener => {
43 | if( ! listener.filter(event) ) return;
44 |
45 | const msg = listener.print(event);
46 |
47 | const data = {
48 | user: get_prop_val(event, 'auth.credentials.provider_username') || 'Anonymous',
49 | };
50 |
51 | const ip = get_prop_val(event, 'info.remoteAddress');
52 | if( ip ) {
53 | data.ip = ip;
54 | }
55 |
56 | listener.logger.info(msg, data);
57 | });
58 | }
59 |
60 | function create_loggers() {
61 | LISTENERS.forEach(listener => {
62 | listener.logger =
63 | new (winston.Logger)({
64 | transports: [
65 | new (winston.transports.File)({ filename: listener.file})
66 | ]
67 | })
68 | });
69 | }
70 |
71 | function get_prop_val(obj, key_chain, return_type) {
72 | const keys = key_chain.split('.');
73 | keys.forEach((key, i) =>
74 | obj = obj[key] || ( i===keys.length-1 ? null : {} ) );
75 | if( return_type && (obj===null || obj.constructor !== return_type ) ) {
76 | return new return_type();
77 | }
78 | return obj;
79 | }
80 |
81 |
--------------------------------------------------------------------------------
/server/http/static_dir.js:
--------------------------------------------------------------------------------
1 | const CLIENT_STATIC_DIR = require('path').join(__dirname, '../../client/dist/static/');
2 |
3 | module.exports = function(server) {
4 | server.route({
5 | method: 'GET',
6 | path: '/static/{param*}',
7 | handler: {
8 | directory: {
9 | path: CLIENT_STATIC_DIR,
10 | index: [],
11 | etagMethod: false,
12 | }
13 | },
14 | });
15 |
16 | };
17 |
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | const Promise = require('bluebird'); Promise.longStackTraces();
2 |
3 | if( process.env.SERVER_DOWN ) {
4 | // show server down notice
5 | require('./server_down');
6 | } else {
7 | const bootup_request_date = new Date();
8 |
9 | // install sentry
10 | require('./util/error_tracker').install();
11 |
12 | // throw an exception if an environment variable is missing
13 | require('./util/env.js');
14 |
15 | const shutdown_args = {bootup_request_date};
16 | end_of_life(shutdown_args);
17 |
18 | // setup database connection
19 | shutdown_args.Thing = require('./database');
20 |
21 | // start http server
22 | const http_server_bootup = require('./http');
23 | http_server_bootup.then(server => shutdown_args.server = server);
24 | start_of_life({http_server_bootup, bootup_request_date});
25 | }
26 |
27 | function start_of_life({http_server_bootup, bootup_request_date}) {
28 | http_server_bootup
29 | .then(server => {
30 | console.log([
31 | 'Server bootup done',
32 | 'uri: '+ server.info.uri,
33 | 'Bootup duration: '+(new Date()-bootup_request_date)+'ms',
34 | ].join(', '));
35 |
36 | // let pm2 now that the process is ready
37 | process.send('ready');
38 | });
39 | }
40 | function end_of_life(shutdown_args) {
41 | const {bootup_request_date} = shutdown_args;
42 |
43 | ['SIGINT', 'SIGTERM']
44 | .forEach(SIG => process.on(SIG, err => stop(SIG, err)));
45 |
46 | function stop(SIG, err) {
47 | const {server, Thing} = shutdown_args;
48 |
49 | const signal_date = new Date();
50 |
51 | const age = (signal_date - bootup_request_date)/(60*1000);
52 | console.log('Server shutdown -- signal `'+SIG+'` received, pid: '+process.pid+', lived for: '+age+'m');
53 | if( err ) {
54 | console.log('Signal error: '+err);
55 | }
56 |
57 | return (
58 | Promise.resolve()
59 | .then(() => {
60 | if( ! server ) {
61 | console.log('Server shutdown -- Error: `server` missing');
62 | return;
63 | }
64 | return (
65 | server.stop({timeout: 10 * 1000})
66 | .then(err => {
67 | if( err ) {
68 | console.log('Server shutdown -- http stop error: '+err);
69 | }
70 | console.log('Server shutdown -- http stop done, pid: '+process.pid+', after: '+(new Date() - signal_date)+'ms');
71 | })
72 | )
73 | })
74 | .then(() => {
75 | if( ! Thing ) {
76 | console.log('Server shutdown -- Error: `Thing` missing');
77 | return;
78 | }
79 | return (
80 | Thing.database.close_connections()
81 | .then(() => {
82 | console.log('Server shutdown -- pg connection closing done, after: '+(new Date() - signal_date)+'ms');
83 | })
84 | );
85 | })
86 | .then(() => {
87 | console.log('Server shutdown -- done, pid: '+process.pid+', took: '+(new Date() - signal_date)+'ms');
88 | process.exit(err?1:0);
89 | })
90 | );
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "assertion-soft": "*",
4 | "atob": "*",
5 | "babel-polyfill": "*",
6 | "babel-preset-es2016": "*",
7 | "babel-preset-es2017": "*",
8 | "babel-preset-node6": "*",
9 | "babel-preset-react": "*",
10 | "babel-preset-stage-2": "*",
11 | "babel-register": "*",
12 | "bell": "*",
13 | "better-assert": "*",
14 | "bluebird": "*",
15 | "boom": "*",
16 | "chalk": "*",
17 | "cheerio": "*",
18 | "clean-sentence": "*",
19 | "deasync": "*",
20 | "hapi": "*",
21 | "hapi-auth-cookie": "*",
22 | "inert": "*",
23 | "knex": "*",
24 | "node-uuid": "*",
25 | "object-sizeof": "*",
26 | "pg": "*",
27 | "promise-serial": "*",
28 | "raven": "*",
29 | "react": "*",
30 | "react-dom": "*",
31 | "remark": "*",
32 | "request": "*",
33 | "request-promise": "*",
34 | "timerlog": "*",
35 | "tlds": "*",
36 | "validator": "*",
37 | "winston": "*"
38 | },
39 | "devDependencies": {
40 | "chai": "*",
41 | "mocha": "*",
42 | "pm2": "*"
43 | },
44 | "scripts": {
45 | "down": "npm run start_nodejs -- --env maintenance",
46 | "up": "npm run prod",
47 | "test": "./node_modules/mocha/bin/mocha",
48 | "dev": "npm run start_postgres && npm run start_nodejs_dev",
49 | "prod": "npm run redirect_port && npm run start_postgres && npm run start_nodejs_prod",
50 | "start_nodejs_dev": "npm run start_nodejs",
51 | "start_nodejs_prod": "npm run start_nodejs -- --env production",
52 | "start_nodejs": "./node_modules/pm2/bin/pm2 startOrReload process.pm2.json",
53 | "start_postgres": "sudo /etc/init.d/postgresql start",
54 | "redirect_port": "sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 8081; (exit 0)"
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/server/process.pm2.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server",
3 | "script": "index.js",
4 | "instances": 1,
5 | "exec_mode": "cluster",
6 | "wait_ready": true,
7 | "listen_timeout": 10000,
8 | "env": {
9 | "kill_timeout": 0,
10 | "node_args": ["--max_old_space_size=550"],
11 | "NODE_ENV": "development",
12 | "SERVER_DOWN": "",
13 | "TIMERLOG": "1",
14 | "TIMERLOG_CLIENT": "0",
15 | "TIMERLOG_DB_PROCESSING": "1",
16 | "watch": true,
17 | "max_memory_restart": "500000000"
18 | },
19 | "env_production" : {
20 | "kill_timeout": 15000,
21 | "node_args": ["--max_old_space_size=700"],
22 | "NODE_ENV": "production",
23 | "SERVER_DOWN": "",
24 | "TIMERLOG": "0",
25 | "TIMERLOG_NEW_VISIT": "1",
26 | "TIMERLOG_SLOWINESS_TRACKER": "1",
27 | "TIMERLOG_DB_PROCESSING": "0",
28 | "watch": false,
29 | "max_memory_restart": "650000000"
30 | },
31 | "env_maintenance" : {
32 | "SERVER_DOWN": "Devarchy is offline for an update. It will be back online in a moment."
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/server/server_down.js:
--------------------------------------------------------------------------------
1 | const hapi = require('hapi');
2 | const assert = require('assert');
3 | const config = require('./http/config');
4 |
5 | const server = new hapi.Server();
6 |
7 | assert(process.env.SERVER_DOWN);
8 |
9 | server.connection({
10 | host: config.host,
11 | port: config.port,
12 | });
13 |
14 | server.route({
15 | method: 'GET',
16 | path: '/{path*}',
17 | handler: function (request, reply) {
18 | reply(process.env.SERVER_DOWN).code(503);
19 | }
20 | });
21 |
22 | server.start((err) => {
23 | if (err) {
24 | throw err;
25 | }
26 | console.log("Server down notice `"+process.env.SERVER_DOWN+"` shown at: "+server.info.uri);
27 | });
28 |
--------------------------------------------------------------------------------
/server/test/mocha.opts:
--------------------------------------------------------------------------------
1 | --bail
2 |
--------------------------------------------------------------------------------
/server/util/b64_unicode.js:
--------------------------------------------------------------------------------
1 | const atob = require('atob');
2 |
3 | module.exports = {
4 | b64DecodeUnicode,
5 | };
6 |
7 | // atob is not enough for unicode chars;
8 | // - http://stackoverflow.com/questions/30106476/using-javascripts-atob-to-decode-base64-doesnt-properly-decode-utf-8-strings
9 | function b64DecodeUnicode(str) {
10 | return decodeURIComponent(Array.prototype.map.call(atob(str), function(c) {
11 | return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
12 | }).join(''));
13 | }
14 |
--------------------------------------------------------------------------------
/server/util/deepFreeze.js:
--------------------------------------------------------------------------------
1 | module.exports = deepFreeze;
2 |
3 | function deepFreeze(obj) {
4 |
5 | if( [null, undefined].includes(obj) ) {
6 | return obj;
7 | }
8 |
9 | // Retrieve the property names defined on obj
10 | var propNames = Object.getOwnPropertyNames(obj);
11 |
12 | // Freeze properties before freezing self
13 | propNames.forEach(function(name) {
14 | var prop = obj[name];
15 |
16 | // Freeze prop if it is an object
17 | if (typeof prop == 'object' && prop !== null)
18 | deepFreeze(prop);
19 | });
20 |
21 | // Freeze self (no-op if already frozen)
22 | return Object.freeze(obj);
23 | }
24 |
--------------------------------------------------------------------------------
/server/util/ensure_database_connection.js:
--------------------------------------------------------------------------------
1 | const assert = require('assert');
2 | const assert_soft = require('assertion-soft');
3 | const knex_module = require('knex');
4 | const conn = require('../database/connection');
5 |
6 |
7 | module.exports = ensure_database_connection;
8 |
9 |
10 | // - PostgreSQL doens't support `CREATE DATABASE IF NOT EXISTS db_name;`
11 | // - PostgreSQL seems to require a connection to a database
12 | function ensure_database_connection(connection) {
13 | assert(connection);
14 | assert(connection.database);
15 |
16 | const database_name = connection.database;
17 |
18 | let handle = get_handle(database_name);
19 | let database_newly_created = false;
20 |
21 | return (
22 | handle.raw('select 1+1 as result')
23 | .catch(err => {
24 | if( err.code !== '3D000' ) {
25 | throw err;
26 | };
27 | assert_soft(err.toString().includes('database "'+database_name+'" does not exist'));
28 |
29 | database_newly_created = true;
30 |
31 | const temporary_handle = get_handle();
32 |
33 | return (
34 | temporary_handle
35 | .raw('CREATE DATABASE "'+database_name+'"')
36 | .then(() => {
37 | temporary_handle.destroy();
38 | handle = get_handle(database_name);
39 | })
40 | )
41 | })
42 | .then(() => {
43 | assert(handle);
44 | assert(handle.raw);
45 | return {handle, database_newly_created};
46 | })
47 | )
48 |
49 | function get_handle(database_name='postgres') {
50 | const conn = Object.assign({}, connection);
51 | delete conn.database;
52 | conn.database = database_name;
53 | return (
54 | knex_module({
55 | dialect: 'postgres',
56 | connection: conn,
57 | })
58 | );
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/server/util/env.js:
--------------------------------------------------------------------------------
1 | // `/.env/` is in `.gitignore`, therefore the env variables can be dropped there
2 | const env = require('../../.env');
3 | /*
4 | let env = {};
5 | try {
6 | env = require('../../.env');
7 | }catch(err){
8 | if( err.code !== 'MODULE_NOT_FOUND' ) throw err;
9 | }
10 | */
11 |
12 | Object.keys(env)
13 | .forEach(key => {
14 | env[key] = process.env[key] || env[key];
15 | });
16 |
17 | [
18 | 'POSTGRES_PASSWORD',
19 | 'HAPI_COOKIE_PASSWORD',
20 | 'HAPI_AUTH_PASSWORD',
21 | 'GITHUB_CLIENT_ID',
22 | 'GITHUB_CLIENT_SECRET',
23 | ]
24 | .forEach(key => {
25 | if( ! env[key] ) throw new Error("Environment variable `"+key+"` missing");
26 | });
27 |
28 | module.exports = env;
29 |
--------------------------------------------------------------------------------
/server/util/error_tracker.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | install,
3 | };
4 |
5 | function install() {
6 | if( process.env['NODE_ENV'] !== 'production' ) {
7 | return;
8 | }
9 | const env = require('../util/env');
10 | require('assert')(env.SENTRY_KEY);
11 | var Raven = require('raven');
12 | Raven.config('https://'+env.SENTRY_KEY+'@sentry.io/122313').install();
13 | }
14 |
15 |
--------------------------------------------------------------------------------
/server/util/fetch_with_cache.js:
--------------------------------------------------------------------------------
1 | const long_term_cache = require('./long_term_cache');
2 | const fetch = require('./fetch');
3 | const assert = require('assert');
4 |
5 |
6 | module.exports = long_term_cache({
7 | cache_name: 'fetch',
8 | function_to_cache: fetch,
9 | hash_input: fetch.hash_fetch_input,
10 | entry_expiration: (() => {
11 | const ONE_DAY = 24*60*60*1000;
12 | return ONE_DAY*10;
13 | })(),
14 | });
15 |
--------------------------------------------------------------------------------
/server/util/gitlab-api.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | const assert = require('assertion-soft');
3 | const assert_soft = require('assertion-soft');
4 | const assert_hard = require('assertion-soft/hard');
5 | const env = require('./env');
6 | const fetch_with_cache = require('./fetch_with_cache');
7 | const {b64DecodeUnicode} = require('./b64_unicode');
8 |
9 | module.exports = {
10 | repo: {
11 | get_readme,
12 | },
13 | url: 'https://gitlab.com/',
14 | };
15 |
16 | const PROJECTS_ID_MAP = {
17 | 'brillout/awesome-react-components': '3298016',
18 | };
19 |
20 | function get_readme({
21 | full_name,
22 | max_delay,
23 | markdown_parsed,
24 | cache__entry_expiration,
25 | branch='master',
26 | }) {
27 | assert(full_name);
28 | assert(markdown_parsed===false);
29 |
30 | // - https://gitlab.com/help/api/repository_files.md
31 | // - 'https://gitlab.example.com/api/v4/projects/13083/repository/files/app%2Fmodels%2Fkey%2Erb?ref=master'
32 | return (
33 | request({
34 | full_name,
35 | path: '/repository/files/readme.md',
36 | query_string: {
37 | ref: branch,
38 | }
39 | }, {
40 | max_delay,
41 | cache__entry_expiration,
42 | })
43 | .then(process_file_response)
44 | );
45 | }
46 |
47 | function request({full_name, path, query_string={}}, {max_delay, expected_error_status_codes, cache__entry_expiration}={}) {
48 |
49 | const project_id = PROJECTS_ID_MAP[full_name];
50 | assert_hard(project_id, full_name);
51 |
52 | assert_hard(path.startsWith('/'), path);
53 | path = (
54 | '/projects/'+project_id+path
55 | );
56 |
57 | query_string.private_token = env.GITLAB_PRIVATE_TOKEN;
58 |
59 | const fetch_params = {
60 | url: 'https://gitlab.com/api/v4'+path,
61 | json: true,
62 | timeout: max_delay,
63 | expected_error_status_codes,
64 | query_string,
65 | };
66 |
67 | const entry_expiration = (
68 | cache__entry_expiration ||
69 | 7 * 24*60*60*1000
70 | );
71 |
72 | return (
73 | fetch_with_cache(
74 | Object.assign(
75 | {},
76 | fetch_params,
77 | {long_term_cache__args: {entry_expiration}}
78 | )
79 | )
80 | )
81 | .then(resp => {
82 | assert(resp, resp);
83 | assert(resp.request_url, resp);
84 | assert(resp.response_status_code, resp);
85 | if( (expected_error_status_codes||[]).includes(resp.response_status_code) ) {
86 | return null;
87 | }
88 | assert(resp.response_headers, JSON.stringify(resp, null, 2));
89 | return resp.response_body;
90 | })
91 | .catch(resp => {
92 | assert(resp, resp);
93 | assert(resp.request_url, resp);
94 | assert(resp.stack, resp);
95 | throw resp;
96 | });
97 | }
98 |
99 | function process_file_response(result) {
100 | if( result === null ) return null;
101 |
102 | assert_soft(result.encoding==='base64');
103 |
104 | return b64DecodeUnicode(result.content);
105 | }
106 |
107 |
108 |
--------------------------------------------------------------------------------
/server/util/multiline_tag.js:
--------------------------------------------------------------------------------
1 | const assert = require('assert');
2 |
3 | module.exports = ml;
4 |
5 | /*
6 | console.log(ml`
7 | test
8 | bla {
9 | uheiruhew
10 | eurh {
11 | var: ${1+1}
12 | }
13 | }
14 | `, 'end'
15 | );
16 | //*/
17 |
18 | function ml (strings, ...var_vals) {
19 |
20 | const str = (() => {
21 | let str = '';
22 | strings
23 | .forEach((s, i) => {
24 | str += s + (i===strings.length-1 ? '' : var_vals[i]);
25 | });
26 | return str;
27 | })();
28 |
29 | const lines = (() => {
30 | let lines = str.split('\n');
31 | assert(is_only_space(lines[0]));
32 | if( is_only_space(lines.slice(-1)[0]) ) {
33 | lines = lines.slice(0, -1);
34 | }
35 | lines = lines.slice(1);
36 | return lines;
37 | })();
38 |
39 | const code_indent = (() => {
40 | let number_of_spaces = Infinity;
41 | lines
42 | .filter(line => !is_only_space(line))
43 | .forEach(line => {
44 | const m = line.match(/^\s*/);
45 | assert(m.length===1);
46 | const line_indent = m[0];
47 | assert(line_indent.constructor===String);
48 | number_of_spaces = Math.min(number_of_spaces, line_indent.length);
49 | });
50 | if( number_of_spaces === Infinity ) {
51 | return '';
52 | }
53 | return (
54 | Array.apply(null, {length: number_of_spaces}).join(' ')+' '
55 | );
56 | })();
57 |
58 | return (
59 | lines
60 | .map(line => line.startsWith(code_indent)?line.slice(code_indent.length):line)
61 | .join('\n')
62 | );
63 |
64 | function is_only_space(str) {
65 | return /^\s*$/.test(str);
66 | }
67 |
68 | }
69 |
--------------------------------------------------------------------------------
/server/util/nodejs_hash.js:
--------------------------------------------------------------------------------
1 | const assert = require('assert');
2 | const crypto = require('crypto');
3 |
4 | module.exports = function nodejs_hash(obj) {
5 | assert([String, Object, Array].includes(obj.constructor));
6 | const str = (
7 | obj.constructor===String ?
8 | obj :
9 | // not deterministic by spec, but deterministic in v8?
10 | JSON.stringify(obj)
11 | );
12 | return (
13 | crypto
14 | .createHash('md5')
15 | .update(str, 'utf8')
16 | .digest('base64')
17 | .replace(/=+$/, '')
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/server/util/normalize_url.js:
--------------------------------------------------------------------------------
1 | ../../client/src/js/util/normalize_url.js
--------------------------------------------------------------------------------
/server/util/npm-api.js:
--------------------------------------------------------------------------------
1 | const fetch = require('./fetch_with_cache');
2 | const assert = require('assert');
3 | const is_npm_package_name_valid = require('./npm_package_name_validation').is_npm_package_name_valid;
4 |
5 |
6 | module.exports = {
7 | get_package_json: npm_package_name => retrive_package_json(npm_package_name),
8 | url: 'https://www.npmjs.com/package/',
9 | is_npm_package_name_valid,
10 | };
11 |
12 | function retrive_package_json(npm_package_name) {
13 | const url = 'http://registry.npmjs.org/'+encodeURIComponent(npm_package_name).replace(/^%40/, '@');
14 |
15 | return (
16 | fetch({
17 | url,
18 | json: true,
19 | pushy: true,
20 | })
21 | .then(resp => {
22 | assert(
23 | resp,
24 | [
25 | 'npm_package_name: '+npm_package_name,
26 | 'resp: '+resp,
27 | ].join('\n')
28 | );
29 | assert(resp.response_body, npm_package_name);
30 | return resp.response_body;
31 | })
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/server/util/npm_package_name_validation.js:
--------------------------------------------------------------------------------
1 | ../../client/src/js/util/npm_package_name_validation.js
--------------------------------------------------------------------------------
/server/util/parse_markdown_catalog/index.js:
--------------------------------------------------------------------------------
1 | const assert = require('assert');
2 | const options = require('./util/options');
3 | const log = require('./util/log');
4 | const parse_markdown = require('./parse_markdown');
5 | const apply_processors = require('./linear_processing/apply_processors');
6 | const categorize = require('./categorize');
7 |
8 | handle_interface();
9 |
10 | function parse_markdown_catalog(contents, {debug=false, mode='strict', categories_to_include, processors, entries_to_prune}={}) {
11 | ['strict', 'loose', 'silent'].includes(mode);
12 |
13 | options.debug = debug;
14 | contents = contents.toString();
15 |
16 | let linear_info = parse_markdown(contents);
17 | apply_processors(linear_info, processors);
18 |
19 | if( entries_to_prune ) {
20 | linear_info = linear_info.filter(entry => !entries_to_prune(entry));
21 | }
22 |
23 | const categories = categorize(linear_info, {mode, categories_to_include});
24 |
25 | if( mode!=='silent' ) {
26 | validate(categories);
27 | }
28 |
29 | log(JSON.stringify(categories, null, 2));
30 |
31 | return categories;
32 | }
33 |
34 | function handle_interface() {
35 | if( require.main !== module ) {
36 | as_module();
37 | } else {
38 | as_cli();
39 | }
40 |
41 | return;
42 |
43 | function as_module() {
44 | module.exports = parse_markdown_catalog;
45 | }
46 |
47 | function as_cli() {
48 | const Promise = require('bluebird'); Promise.longStackTraces();
49 | const readFile = Promise.promisify(require("fs").readFile);
50 | if( process.argv.length !== 3 ) {
51 | throw new Error(process.argv.length<3?'missing argument':'too many arguments');
52 | }
53 | const path = process.argv[2];
54 | readFile(path)
55 | .then(contents => {
56 | const categories = parse_markdown_catalog(contents, {debug: false, mode: 'loose'});
57 | // categories.forEach(c => {c.resources = c.resources.length});
58 | console.log(JSON.stringify(categories, null, 2));
59 | });
60 | }
61 | }
62 |
63 | function validate(categories) {
64 | categories.forEach(category => {
65 | category.resources
66 | .forEach(resource => {
67 |
68 | assert(resource.as_website_url);
69 |
70 | assert(!resource.as_npm_package || resource.as_github_repository);
71 |
72 | // make sure that GitHub URLs are repositories
73 | assert(!resource.as_website_url.resource_url.startsWith('https://github.com') || resource.as_github_repository, JSON.stringify(resource, null, 2));
74 |
75 | });
76 | });
77 |
78 | }
79 |
--------------------------------------------------------------------------------
/server/util/parse_markdown_catalog/linear_processing/apply_processors.js:
--------------------------------------------------------------------------------
1 | const assert = require('assert');
2 | const processors = require('./processors');
3 |
4 | module.exports = function(linear_info, processor_options={}) {
5 | linear_info.forEach(info => {
6 | info.processed = {};
7 | processors
8 | .forEach(processor => {
9 | const info_title = processor.info_title;
10 | assert(info_title);
11 | const res = processor.process(info.type, info.raw_data, processor_options[info_title]);
12 | if( res !== null ) {
13 | info.processed[info_title] = res;
14 | }
15 | });
16 | });
17 | };
18 |
--------------------------------------------------------------------------------
/server/util/parse_markdown_catalog/linear_processing/processors/github_entry.js:
--------------------------------------------------------------------------------
1 | const assert = require('assert');
2 | const is_github_url = require('../../util/is_github_url');
3 |
4 |
5 | module.exports = {
6 | process,
7 | info_title: 'as_github_repository',
8 | };
9 |
10 |
11 | function process(type, raw_data, options) {
12 | if( type === 'link' ) {
13 | return process_link(raw_data, options);
14 | }
15 | return null;
16 | }
17 |
18 | function process_link(raw_data, options={}) {
19 | const link = raw_data.url;
20 |
21 | if( raw_data.texts.before!=='' ) {
22 | return null;
23 | }
24 |
25 | const title = raw_data.texts.inside;
26 | if( !title ) {
27 | return null;
28 | }
29 |
30 | const github_full_name = is_github_url(link, {expect_correct_url: false, correct_wrong_url: options.correct_wrong_url});
31 | if( !github_full_name ) {
32 | return null;
33 | }
34 |
35 | return {
36 | github_full_name,
37 | title,
38 | };
39 | }
40 |
--------------------------------------------------------------------------------
/server/util/parse_markdown_catalog/linear_processing/processors/index.js:
--------------------------------------------------------------------------------
1 | module.exports = (
2 | [
3 | require('./npm_entry'),
4 | require('./web_entry'),
5 | require('./github_entry'),
6 | ]
7 | );
8 |
--------------------------------------------------------------------------------
/server/util/parse_markdown_catalog/linear_processing/processors/npm_entry.js:
--------------------------------------------------------------------------------
1 | const assert = require('assert');
2 | const is_github_url = require('../../util/is_github_url');
3 | const npm_package_name_validation = require('../../../npm_package_name_validation');
4 |
5 |
6 | module.exports = {
7 | process,
8 | info_title: 'as_npm_package',
9 | };
10 |
11 |
12 | function process(type, raw_data) {
13 | if( type === 'link' ) {
14 | return process_link(raw_data);
15 | }
16 | return null;
17 | }
18 |
19 | function process_link(raw_data) {
20 | const link = raw_data.url;
21 | const text = raw_data.texts.inside;
22 |
23 | const github_full_name = is_github_url(link, {expect_correct_url: false});
24 |
25 | let npm_package_name = null;
26 | if( npm_package_name_validation.is_npm_package_name_valid(text) ) {
27 | npm_package_name = text;
28 | }
29 |
30 | if( !github_full_name || !npm_package_name ) {
31 | return null;
32 | }
33 |
34 | return {
35 | github_full_name,
36 | npm_package_name,
37 | };
38 | }
39 |
--------------------------------------------------------------------------------
/server/util/parse_markdown_catalog/linear_processing/processors/web_entry.js:
--------------------------------------------------------------------------------
1 | const assert = require('assert');
2 | const validator = require('validator');
3 | const tlds = require('tlds');
4 | const normalize_url = require('../../../normalize_url');
5 |
6 |
7 | module.exports = {
8 | process,
9 | info_title: 'as_website_url',
10 | };
11 |
12 |
13 | function process(type, raw_data) {
14 | if( type === 'link' ) {
15 | return process_link(raw_data);
16 | }
17 | if( type === 'header') {
18 | return null;
19 | }
20 | if( type === 'description') {
21 | return null;
22 | }
23 | assert(false);
24 | }
25 |
26 |
27 | function process_link(raw_data) {
28 | const DESCRIPTION_PREFIX = ' - ';
29 | validate(!!raw_data.url, "URL shouldn't be empty");
30 | validate(!!raw_data.texts.inside, "URL text shouldn't be empty");
31 | if( ! normalize_url.is_url(raw_data.url) ) {
32 | return null;
33 | }
34 | // validate(raw_data.texts.before==='', "URL shouldn't be preceded by any text");
35 | if( raw_data.texts.before!=='' ) {
36 | return null;
37 | }
38 | validate(validator.isURL(raw_data.url,{allow_underscores: true}), "Doesn't seem to be an URL: `"+raw_data.url+"`");
39 | const resource_url = raw_data.url;
40 | const dn = normalize_url(resource_url).split('/')[0];
41 | validate(validator.isFQDN(dn), "Doesn't seem to be a valid domain: `"+dn+"`");
42 | validate(tlds.includes(dn.split('.').slice(-1)[0]), "Doesn't seem to be a valid TLD for: `"+dn+"`");
43 | const title = raw_data.texts.inside;
44 |
45 | let description = raw_data.texts.after;
46 | if( description.startsWith(DESCRIPTION_PREFIX) ) {
47 | description = description.slice(DESCRIPTION_PREFIX.length);
48 | }
49 |
50 | delete raw_data;
51 |
52 | return {
53 | resource_url,
54 | title,
55 | description,
56 | };
57 |
58 | function validate(passed, msg){
59 | const last_line = raw_data.last_line;
60 | assert((last_line||{}).constructor===Number, JSON.stringify(raw_data, null, 2));
61 | if( !passed ) {
62 | const out =
63 | 'Error at line '+last_line+': '+msg +'\n'+
64 | JSON.stringify(raw_data,null,2);
65 | throw new Error(out);
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/server/util/parse_markdown_catalog/tests/test_description.md:
--------------------------------------------------------------------------------
1 | ## Test Category 1
2 |
3 | *description 1*
4 |
5 | - [test-package-1](https://github.com/testuser1/test_package_1)
6 |
7 |
8 |
9 |
10 |
11 | ## Test Category 2
12 |
13 |
14 | Multi line
15 | Description for
16 |
17 | Category 2
18 |
19 |
20 | - [test-package-2](https://github.com/testuser2/test_package_2)
21 |
22 |
23 |
24 |
25 | ## Test Category 3
26 |
27 | A
28 | Description for
29 |
30 | cat 3
31 |
32 | - [test-package-3](https://github.com/testuser2/test_package_3)
33 |
34 |
35 | ## Test Category 4
36 |
37 |
38 | Description with tags.
39 |
40 | \[tag1\]\[tag2\]
41 |
42 |
43 | - [test-package-3](https://github.com/testuser2/test_package_3)
44 |
45 |
46 |
47 | ## Test Category 5
48 |
49 |
50 | Descri
51 |
52 | With markdown *emphasis* and `var code;`.
53 |
54 |
55 | - [test-package-3](https://github.com/testuser2/test_package_3)
56 |
--------------------------------------------------------------------------------
/server/util/parse_markdown_catalog/util/is_github_url.js:
--------------------------------------------------------------------------------
1 | const assert = require('assert');
2 | const assert_hard = require('assert');
3 |
4 | const GITHUB_URL_START = 'https://github.com/';
5 |
6 | module.exports = (url, {throw_on_wrong_url=true, correct_wrong_url=false, expect_correct_url=false, }={}) => {
7 | if( ! url.startsWith(GITHUB_URL_START) ) {
8 | return null;
9 | }
10 | const github_full_name = url.slice(GITHUB_URL_START.length);
11 | const url_is_correct = github_full_name.split('/').length === 2;
12 | if( expect_correct_url && ! url_is_correct ) {
13 | const msg = 'Losing path information of `'+github_full_name+'`';
14 | if( throw_on_wrong_url !== false ) {
15 | assert_hard(false, msg);
16 | } else {
17 | console.warn(msg);
18 | }
19 | }
20 | if( url_is_correct ) {
21 | return github_full_name;
22 | }
23 | if( correct_wrong_url ) {
24 | return github_full_name.split('/').slice(0,2).join('/');
25 | }
26 | return null;
27 | };
28 |
--------------------------------------------------------------------------------
/server/util/parse_markdown_catalog/util/log.js:
--------------------------------------------------------------------------------
1 | const options = require('./options');
2 |
3 | module.exports = function log() {
4 | if( ! options.debug ) {
5 | return
6 | }
7 | console.log.apply(console, arguments);
8 | };
9 |
--------------------------------------------------------------------------------
/server/util/parse_markdown_catalog/util/options.js:
--------------------------------------------------------------------------------
1 | module.exports = {};
2 |
--------------------------------------------------------------------------------
/server/util/turn_into_error_object.js:
--------------------------------------------------------------------------------
1 | const assert = require('assert');
2 |
3 |
4 | module.exports = turn_into_error_object;
5 | turn_into_error_object.is_error_object = is_error_object;
6 |
7 |
8 | const error_props = ['stack', 'message', 'name',];
9 |
10 | function turn_into_error_object(obj, err_obj=new Error()) {
11 | assert(is_error_object(err_obj), err_obj);
12 | error_props.forEach(prop => {
13 | Object.defineProperty(obj, prop, {enumerable: false, writable: true, configurable: true, value: err_obj[prop]});
14 | });
15 | assert(is_error_object(obj), obj);
16 | return obj;
17 | }
18 |
19 | function is_error_object(obj) {
20 | return (
21 | typeof obj === "object" &&
22 | error_props.every(prop => typeof obj[prop] === "string" || !!obj[prop])
23 | );
24 | };
25 |
--------------------------------------------------------------------------------