├── .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 | <span id="replaceme"></span> 11 | 14 | <% for (var css in htmlWebpackPlugin.files.css) { %> 15 | 16 | <% } %> 17 | 18 | 19 | 20 | 21 |
22 | 23 |
24 | 25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | 33 |
34 |
35 |
36 |
37 |
38 | 39 |
40 |
41 |
42 |
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 |
  1. 31 | 32 | Further catalogs 33 | 34 | {' '}to cover more frontend development areas. 35 |
  2. 36 |
  3. 37 | To be determined. 38 |
  4. 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