Repository: lbryio/spee.ch Branch: master Commit: 99c7b087d751 Files: 442 Total size: 537.4 KB Directory structure: gitextract_v9i4r49y/ ├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── .nvmrc ├── .prettierrc.json ├── .sequelizerc ├── .travis.yml ├── LICENSE ├── README.md ├── changelog.md ├── cli/ │ ├── configure.js │ ├── defaults/ │ │ ├── chainqueryConfig.json │ │ ├── lbryConfig.json │ │ ├── loggerConfig.json │ │ ├── mysqlConfig.json │ │ ├── siteConfig.json │ │ └── slackConfig.json │ └── questions/ │ ├── mysqlQuestions.js │ └── siteQuestions.js ├── client/ │ ├── scss/ │ │ ├── _asset-blocked.scss │ │ ├── _asset-display.scss │ │ ├── _asset-preview.scss │ │ ├── _body.scss │ │ ├── _button-primary.scss │ │ ├── _button-secondary.scss │ │ ├── _button.scss │ │ ├── _channel-claims-display.scss │ │ ├── _claim-pending.scss │ │ ├── _click-to-copy.scss │ │ ├── _dropzone.scss │ │ ├── _form-feedback.scss │ │ ├── _form.scss │ │ ├── _horizontal-split.scss │ │ ├── _html.scss │ │ ├── _input.scss │ │ ├── _label.scss │ │ ├── _link.scss │ │ ├── _markdown.scss │ │ ├── _media-queries.scss │ │ ├── _nav-bar.scss │ │ ├── _page-content.scss │ │ ├── _page-layout-show-lite.scss │ │ ├── _page-layout.scss │ │ ├── _progress-bar.scss │ │ ├── _publish-disabled-message.scss │ │ ├── _publish-preview.scss │ │ ├── _publish-status.scss │ │ ├── _publish-url-input.scss │ │ ├── _react-app.scss │ │ ├── _reset.scss │ │ ├── _row.scss │ │ ├── _select.scss │ │ ├── _share-buttons.scss │ │ ├── _social-share-link.scss │ │ ├── _space-around.scss │ │ ├── _space-between.scss │ │ ├── _text.scss │ │ ├── _textarea.scss │ │ ├── _tooltip.scss │ │ ├── _variables.scss │ │ ├── _video.scss │ │ ├── all.scss │ │ └── font/ │ │ ├── Lekton/ │ │ │ └── OFL.txt │ │ └── _font.scss │ └── src/ │ ├── actions/ │ │ ├── channel.js │ │ ├── channelCreate.js │ │ ├── index.js │ │ ├── publish.js │ │ └── show.js │ ├── api/ │ │ ├── assetApi.js │ │ ├── authApi.js │ │ ├── channelApi.js │ │ ├── fileApi.js │ │ ├── homepageApi.js │ │ └── specialAssetApi.js │ ├── app.js │ ├── channels/ │ │ └── publish.js │ ├── components/ │ │ ├── AboutSpeechDetails/ │ │ │ └── index.jsx │ │ ├── AboutSpeechOverview/ │ │ │ └── index.jsx │ │ ├── ActiveStatusBar/ │ │ │ └── index.jsx │ │ ├── AssetInfoFooter/ │ │ │ └── index.js │ │ ├── AssetPreview/ │ │ │ └── index.jsx │ │ ├── AssetShareButtons/ │ │ │ └── index.js │ │ ├── ButtonPrimary/ │ │ │ └── index.jsx │ │ ├── ButtonPrimaryJumbo/ │ │ │ └── index.jsx │ │ ├── ButtonSecondary/ │ │ │ └── index.jsx │ │ ├── ChannelAbout/ │ │ │ └── index.jsx │ │ ├── ChannelCreateNameInput/ │ │ │ └── index.jsx │ │ ├── ChannelCreatePasswordInput/ │ │ │ └── index.jsx │ │ ├── ChannelInfoDisplay/ │ │ │ └── index.jsx │ │ ├── ChannelLoginNameInput/ │ │ │ └── index.jsx │ │ ├── ChannelLoginPasswordInput/ │ │ │ └── index.jsx │ │ ├── ChannelSelectDropdown/ │ │ │ └── index.jsx │ │ ├── ChooseAnonymousPublishRadio/ │ │ │ └── index.jsx │ │ ├── ChooseChannelPublishRadio/ │ │ │ └── index.jsx │ │ ├── ClickToCopy/ │ │ │ └── index.jsx │ │ ├── DropzoneDropItDisplay/ │ │ │ └── index.jsx │ │ ├── DropzoneInstructionsDisplay/ │ │ │ └── index.jsx │ │ ├── DropzonePreviewImage/ │ │ │ └── index.jsx │ │ ├── ErrorBoundary/ │ │ │ └── index.jsx │ │ ├── ExpandingTextArea/ │ │ │ └── index.jsx │ │ ├── FileViewer/ │ │ │ └── index.jsx │ │ ├── FormFeedbackDisplay/ │ │ │ └── index.jsx │ │ ├── GAListener/ │ │ │ └── index.jsx │ │ ├── HorizontalSplit/ │ │ │ └── index.jsx │ │ ├── InactiveStatusBar/ │ │ │ └── index.jsx │ │ ├── Label/ │ │ │ └── index.jsx │ │ ├── Logo/ │ │ │ └── index.jsx │ │ ├── Memeify/ │ │ │ ├── EditableFontface/ │ │ │ │ └── index.js │ │ │ ├── FontFaces/ │ │ │ │ ├── .gitkeep │ │ │ │ ├── GreenMachine.js │ │ │ │ ├── Inferno.js │ │ │ │ ├── Lazer.js │ │ │ │ ├── Neon.js │ │ │ │ ├── OldBlue.js │ │ │ │ ├── Outline.js │ │ │ │ ├── RetroRainbow.js │ │ │ │ ├── TheSpecial.js │ │ │ │ └── VaporWave.js │ │ │ ├── RichDraggable/ │ │ │ │ └── index.js │ │ │ └── index.js │ │ ├── NavBar/ │ │ │ └── index.jsx │ │ ├── NavBarChannelOptionsDropdown/ │ │ │ └── index.jsx │ │ ├── PageLayout/ │ │ │ └── index.jsx │ │ ├── PageLayoutShowLite/ │ │ │ └── index.jsx │ │ ├── ProgressBar/ │ │ │ └── index.jsx │ │ ├── PublishDescriptionInput/ │ │ │ └── index.jsx │ │ ├── PublishFinePrint/ │ │ │ └── index.jsx │ │ ├── PublishLicenseInput/ │ │ │ └── index.jsx │ │ ├── PublishLicenseUrlInput/ │ │ │ └── index.jsx │ │ ├── PublishNsfwInput/ │ │ │ └── index.jsx │ │ ├── PublishPreview/ │ │ │ └── index.jsx │ │ ├── PublishUrlMiddleDisplay/ │ │ │ └── index.jsx │ │ ├── Row/ │ │ │ └── index.jsx │ │ ├── RowLabeled/ │ │ │ └── index.jsx │ │ ├── SocialShareLink/ │ │ │ └── index.jsx │ │ ├── SpaceAround/ │ │ │ └── index.jsx │ │ ├── SpaceBetween/ │ │ │ └── index.jsx │ │ └── VerticalSplit/ │ │ └── index.jsx │ ├── constants/ │ │ ├── asset_display_states.js │ │ ├── channel_action_types.js │ │ ├── channel_create_action_types.js │ │ ├── confirmation_messages.js │ │ ├── publish_action_types.js │ │ ├── publish_channel_select_states.js │ │ ├── publish_claim_states.js │ │ ├── publish_license_urls.js │ │ ├── show_action_types.js │ │ └── show_request_types.js │ ├── containers/ │ │ ├── AssetBlocked/ │ │ │ ├── index.js │ │ │ └── view.jsx │ │ ├── AssetDisplay/ │ │ │ ├── index.js │ │ │ └── view.jsx │ │ ├── AssetInfo/ │ │ │ ├── index.js │ │ │ └── view.jsx │ │ ├── AssetTitle/ │ │ │ ├── index.js │ │ │ └── view.jsx │ │ ├── ChannelClaimsDisplay/ │ │ │ ├── index.js │ │ │ └── view.jsx │ │ ├── ChannelCreateForm/ │ │ │ ├── index.js │ │ │ └── view.jsx │ │ ├── ChannelLoginForm/ │ │ │ ├── index.js │ │ │ └── view.jsx │ │ ├── ChannelSelect/ │ │ │ ├── index.js │ │ │ └── view.jsx │ │ ├── ChannelTools/ │ │ │ ├── index.js │ │ │ └── view.jsx │ │ ├── Dropzone/ │ │ │ ├── index.js │ │ │ └── view.jsx │ │ ├── NavigationLinks/ │ │ │ ├── index.jsx │ │ │ └── view.jsx │ │ ├── PublishDetails/ │ │ │ ├── index.js │ │ │ └── view.jsx │ │ ├── PublishDisabledMessage/ │ │ │ ├── index.js │ │ │ └── view.jsx │ │ ├── PublishMetadataInputs/ │ │ │ ├── index.js │ │ │ └── view.jsx │ │ ├── PublishStatus/ │ │ │ ├── index.js │ │ │ └── view.jsx │ │ ├── PublishThumbnailInput/ │ │ │ ├── index.js │ │ │ └── view.jsx │ │ ├── PublishTitleInput/ │ │ │ ├── index.js │ │ │ └── view.jsx │ │ ├── PublishTool/ │ │ │ ├── index.js │ │ │ └── view.jsx │ │ ├── PublishUrlInput/ │ │ │ ├── index.js │ │ │ └── view.jsx │ │ ├── SEO/ │ │ │ ├── index.js │ │ │ └── view.jsx │ │ └── SiteDescription/ │ │ ├── index.jsx │ │ └── view.jsx │ ├── index.js │ ├── pages/ │ │ ├── AboutPage/ │ │ │ └── index.jsx │ │ ├── ContentPageWrapper/ │ │ │ ├── index.jsx │ │ │ └── view.jsx │ │ ├── EditPage/ │ │ │ ├── index.js │ │ │ └── view.jsx │ │ ├── ErrorPage/ │ │ │ └── index.jsx │ │ ├── FaqPage/ │ │ │ └── index.jsx │ │ ├── FourOhFourPage/ │ │ │ └── index.jsx │ │ ├── HomePage/ │ │ │ ├── index.js │ │ │ └── view.jsx │ │ ├── LoginPage/ │ │ │ ├── index.jsx │ │ │ └── view.jsx │ │ ├── MultisitePage/ │ │ │ └── index.jsx │ │ ├── PopularPage/ │ │ │ ├── index.jsx │ │ │ └── view.jsx │ │ ├── ShowAssetDetails/ │ │ │ ├── index.js │ │ │ └── view.jsx │ │ ├── ShowAssetLite/ │ │ │ ├── index.js │ │ │ └── view.jsx │ │ ├── ShowChannel/ │ │ │ ├── index.js │ │ │ └── view.jsx │ │ └── TosPage/ │ │ └── index.jsx │ ├── reducers/ │ │ ├── channel.js │ │ ├── channelCreate.js │ │ ├── index.js │ │ ├── publish.js │ │ ├── show.js │ │ └── site.js │ ├── sagas/ │ │ ├── abandon.js │ │ ├── checkForLoggedInChannel.js │ │ ├── createChannel.js │ │ ├── file.js │ │ ├── index.js │ │ ├── logoutChannel.js │ │ ├── publish.js │ │ ├── rootSaga.js │ │ ├── show_asset.js │ │ ├── show_channel.js │ │ ├── show_special.js │ │ ├── show_uri.js │ │ ├── updateChannelAvailability.js │ │ └── updateClaimAvailability.js │ ├── selectors/ │ │ ├── channel.js │ │ ├── channelCreate.js │ │ ├── publish.js │ │ ├── show.js │ │ └── site.js │ └── utils/ │ ├── createAssetMetaTags.js │ ├── createBasicMetaTags.js │ ├── createChannelMetaTags.js │ ├── createGroupedList.js │ ├── createMetaTags.js │ ├── createMetaTagsArray.js │ ├── createPageTitle.js │ ├── createPermanentURI.js │ ├── determineContentTypeFromExtension.js │ ├── dynamicImport.js │ ├── file.js │ ├── oEmbed.js │ ├── publish.js │ ├── request.js │ └── validate.js ├── customize.md ├── devConfig/ │ ├── sequelizeCliConfig.example.js │ └── testingConfig.example.js ├── docs/ │ ├── settings.md │ ├── setup/ │ │ ├── conf/ │ │ │ ├── caddy/ │ │ │ │ ├── Caddyfile.template │ │ │ │ └── caddy.service │ │ │ ├── lbrynet/ │ │ │ │ ├── lbrynet.service.example │ │ │ │ └── lbrynet.service.template │ │ │ ├── nginx/ │ │ │ │ ├── letsencrypt.conf │ │ │ │ ├── myspeech │ │ │ │ └── ssl.conf │ │ │ └── speech/ │ │ │ ├── chainqueryConfig.json │ │ │ └── speech.service.draft │ │ └── scripts/ │ │ ├── firewall.sh │ │ └── newuser.sh │ └── ubuntuinstall.md ├── fullstart.md ├── lintstagedrc.json ├── nginx_example_config ├── package.json ├── public/ │ ├── bundle/ │ │ └── .gitkeep │ └── robots.txt ├── server/ │ ├── chainquery/ │ │ ├── index.debug.js │ │ ├── index.js │ │ ├── models/ │ │ │ ├── AbnormalClaimModel.js │ │ │ ├── AddressModel.js │ │ │ ├── BlockModel.js │ │ │ ├── ClaimModel.js │ │ │ ├── InputModel.js │ │ │ ├── OutputModel.js │ │ │ ├── SupportModel.js │ │ │ ├── TransactionAddressModel.js │ │ │ └── TransactionModel.js │ │ ├── queries/ │ │ │ ├── abnormalClaimQueries.js │ │ │ ├── addressQueries.js │ │ │ ├── blockQueries.js │ │ │ ├── claimQueries.js │ │ │ ├── inputQueries.js │ │ │ ├── outputQueries.js │ │ │ ├── supportQueries.js │ │ │ ├── transactionAddressQueries.js │ │ │ └── transactionQueries.js │ │ └── tables/ │ │ ├── abnormalClaimTable.js │ │ ├── addressTable.js │ │ ├── blockTable.js │ │ ├── claimTable.js │ │ ├── inputTable.js │ │ ├── outputTable.js │ │ ├── supportTable.js │ │ ├── transactionAddressTable.js │ │ └── transactionTable.js │ ├── controllers/ │ │ ├── api/ │ │ │ ├── blocked/ │ │ │ │ └── index.js │ │ │ ├── channel/ │ │ │ │ ├── availability/ │ │ │ │ │ ├── checkChannelAvailability.js │ │ │ │ │ └── index.js │ │ │ │ ├── claims/ │ │ │ │ │ ├── channelPagination.js │ │ │ │ │ ├── getChannelClaims.js │ │ │ │ │ └── index.js │ │ │ │ ├── data/ │ │ │ │ │ ├── getChannelData.js │ │ │ │ │ └── index.js │ │ │ │ └── shortId/ │ │ │ │ └── index.js │ │ │ ├── claim/ │ │ │ │ ├── abandon/ │ │ │ │ │ └── index.js │ │ │ │ ├── availability/ │ │ │ │ │ ├── checkClaimAvailability.js │ │ │ │ │ └── index.js │ │ │ │ ├── data/ │ │ │ │ │ └── index.js │ │ │ │ ├── get/ │ │ │ │ │ └── index.js │ │ │ │ ├── list/ │ │ │ │ │ └── index.js │ │ │ │ ├── longId/ │ │ │ │ │ └── index.js │ │ │ │ ├── publish/ │ │ │ │ │ ├── authentication.js │ │ │ │ │ ├── createPublishParams.js │ │ │ │ │ ├── createThumbnailPublishParams.js │ │ │ │ │ ├── deleteFile.js │ │ │ │ │ ├── index.js │ │ │ │ │ ├── parsePublishApiRequestBody.js │ │ │ │ │ ├── parsePublishApiRequestBody.test.js │ │ │ │ │ ├── parsePublishApiRequestFiles.js │ │ │ │ │ ├── parsePublishApiRequestFiles.test.js │ │ │ │ │ ├── publish.js │ │ │ │ │ ├── validateFileForPublish.js │ │ │ │ │ └── validateFileTypeAndSize.js │ │ │ │ ├── resolve/ │ │ │ │ │ └── index.js │ │ │ │ ├── shortId/ │ │ │ │ │ └── index.js │ │ │ │ ├── update/ │ │ │ │ │ └── index.js │ │ │ │ └── views/ │ │ │ │ └── index.js │ │ │ ├── file/ │ │ │ │ └── availability/ │ │ │ │ └── index.js │ │ │ ├── homepage/ │ │ │ │ └── data/ │ │ │ │ ├── getChannelData.js │ │ │ │ └── index.js │ │ │ ├── oEmbed/ │ │ │ │ ├── getOEmbedDataForAsset.js │ │ │ │ ├── getOEmbedDataForChannel.js │ │ │ │ ├── index.js │ │ │ │ └── parseSpeechUrl.js │ │ │ ├── special/ │ │ │ │ └── claims/ │ │ │ │ └── index.js │ │ │ ├── tor/ │ │ │ │ └── index.js │ │ │ └── user/ │ │ │ └── password/ │ │ │ └── index.js │ │ ├── assets/ │ │ │ ├── constants/ │ │ │ │ └── request_types.js │ │ │ ├── serveAsset/ │ │ │ │ └── index.js │ │ │ ├── serveByClaim/ │ │ │ │ └── index.js │ │ │ ├── serveByIdentifierAndClaim/ │ │ │ │ └── index.js │ │ │ └── utils/ │ │ │ ├── determineRequestType.js │ │ │ ├── flipClaimNameAndId.js │ │ │ ├── getClaimIdAndServeAsset.js │ │ │ ├── getLocalFileRecord.js │ │ │ ├── logRequestData.js │ │ │ ├── serveFile.js │ │ │ └── transformImage.js │ │ ├── auth/ │ │ │ ├── login/ │ │ │ │ └── index.js │ │ │ ├── logout/ │ │ │ │ └── index.js │ │ │ ├── signup/ │ │ │ │ └── index.js │ │ │ └── user/ │ │ │ └── index.js │ │ ├── pages/ │ │ │ ├── sendReactApp.js │ │ │ └── sendVideoEmbedPage.js │ │ └── utils/ │ │ ├── errorHandlers.js │ │ ├── getClaimId.js │ │ └── redirect.js │ ├── index.js │ ├── lbrynet/ │ │ ├── index.js │ │ └── utils/ │ │ └── handleLbrynetResponse.js │ ├── middleware/ │ │ ├── autoblockPublishMiddleware.js │ │ ├── httpContextMiddleware.js │ │ ├── logMetricsMiddleware.js │ │ ├── multipartMiddleware.js │ │ ├── requestLogger.js │ │ └── torCheckMiddleware.js │ ├── migrations/ │ │ ├── ChangeCertificateColumnTypes2.js │ │ ├── ChangeClaimColumnTypes.js │ │ └── File_AddHeightAndWidthColumn.js │ ├── models/ │ │ ├── blocked.js │ │ ├── certificate.js │ │ ├── channel.js │ │ ├── claim.js │ │ ├── file.js │ │ ├── index.js │ │ ├── metrics.js │ │ ├── tor.js │ │ ├── trending.js │ │ ├── user.js │ │ ├── utils/ │ │ │ ├── createClaimRecordData.js │ │ │ ├── createDatabaseIfNotExists.js │ │ │ ├── createFileRecordData.js │ │ │ ├── returnShortId.js │ │ │ ├── returnShortId.test.js │ │ │ └── trendingAnalysis.js │ │ └── views.js │ ├── render/ │ │ ├── handleShowRender.jsx │ │ └── renderFullPage.js │ ├── routes/ │ │ ├── api/ │ │ │ └── index.js │ │ ├── assets/ │ │ │ └── index.js │ │ ├── auth/ │ │ │ └── index.js │ │ ├── fallback/ │ │ │ └── index.js │ │ ├── index.js │ │ └── pages/ │ │ └── index.js │ ├── speechPassport/ │ │ ├── index.js │ │ └── utils/ │ │ ├── deserializeUser.js │ │ ├── local-login.js │ │ ├── local-signup.js │ │ └── serializeUser.js │ ├── task-scripts/ │ │ ├── update-channel-names.js │ │ └── update-password.js │ ├── utils/ │ │ ├── awaitFileSize.js │ │ ├── blockList.js │ │ ├── configureLogging.js │ │ ├── configureSlack.js │ │ ├── createModuleAliases.js │ │ ├── fetchClaimData.js │ │ ├── getClaimData.js │ │ ├── getMediaDimensions.js │ │ ├── googleAnalytics.js │ │ ├── imageProcessing.js │ │ ├── isRequestLocal.js │ │ ├── isValidQueryObj.js │ │ ├── processTrending.js │ │ └── videoProcessing.js │ └── views/ │ ├── embed.handlebars │ ├── layouts/ │ │ └── embed.handlebars │ └── partials/ │ └── logo.handlebars ├── server.js ├── test/ │ ├── end-to-end/ │ │ └── end-to-end.test.js │ └── module-alias-boilerplate.js ├── utils/ │ ├── checkForLocalConfig.js │ ├── createCanonicalLink.js │ ├── createModuleAliases.js │ ├── isApprovedChannel.js │ ├── lbryUri.js │ └── validateFileForPublish.js ├── webpack/ │ ├── webpack.client.config.js │ └── webpack.server.config.js └── webpack.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .babelrc ================================================ { "presets": ["@babel/env", "@babel/react"], "plugins": ["react-hot-loader/babel", "@babel/plugin-proposal-object-rest-spread"] } ================================================ FILE: .eslintignore ================================================ client/build node_modules/ public/bundle server/render/build server/bundle test/ server/chainquery ================================================ FILE: .eslintrc ================================================ { "parser": "babel-eslint", "extends": ["standard", "standard-jsx"], "env": { "es6": true, "jest": true, "node": true, "browser": true }, "globals": { "GENTLY": true }, "rules": { "no-multi-spaces": 0, "new-cap": 0, "prefer-promise-reject-errors": 0, "no-unused-vars": 0, "standard/object-curly-even-spacing": 0, "handle-callback-err": 0, "one-var": 0, "object-curly-spacing": 0, "comma-dangle": ["error", "always-multiline"], "semi": ["error", "always", { "omitLastInOneLineBlock": true }], "key-spacing": [ "error", { "multiLine": { "beforeColon": false, "afterColon": true }, "align": { "beforeColon": false, "afterColon": true, "on": "colon", "mode": "strict" } } ] } } ================================================ FILE: .gitignore ================================================ .DS_Store *.log .idea/ node_modules client/build site/ devConfig/sequelizeCliConfig.js devConfig/testingConfig.js server/bundle public/bundle/bundle.js public/bundle/bundle.js.map public/bundle/Lekton-* public/bundle/style.css uploads config/ deployment-config.json ================================================ FILE: .npmignore ================================================ client/src server/render/src .babelrc ================================================ FILE: .nvmrc ================================================ v8.12.0 ================================================ FILE: .prettierrc.json ================================================ { "trailingComma": "es5", "printWidth": 100, "singleQuote": true } ================================================ FILE: .sequelizerc ================================================ const path = require('path'); module.exports = { 'config': path.resolve('devConfig', 'sequelizeCliConfig.js'), 'models-path': path.resolve('server', 'models'), 'seeders-path': path.resolve('server', 'seeders'), 'migrations-path': path.resolve('server', 'migrations') } ================================================ FILE: .travis.yml ================================================ dist: xenial #addons: # apt: # sources: # - mysql-5.7-trusty # packages: # - mysql-server # - mysql-client language: node_js node_js: - "lts/dubnium" cache: directories: - "node_modules" #services: # - mysql jobs: include: - stage: "Build" name: "Build and run test environment" before_install: # - sudo mysql -e "use mysql; update user set authentication_string=PASSWORD('password') where User='root'; update user set plugin='mysql_native_password';FLUSH PRIVILEGES;" # - sudo mysql_upgrade -u root -ppassword # - sudo service mysql restart # - mysql -u root -ppassword -e 'CREATE DATABASE IF NOT EXISTS lbry;' # - mysql -u root -ppassword -e "CREATE USER 'lbry'@'localhost' IDENTIFIED BY 'lbry';" # - mysql -u root -ppassword -e "GRANT ALL ON lbry.* TO 'lbry'@'localhost';" # - sudo service mysql restart - dpkg --compare-versions `npm -v` ge 6.4.0 || npm i -g npm@^6.4.0 install: - npm i script: - cp ./cli/defaults/* ./site/config/ - | echo '{ "sessionKey": "session", "masterPassword": false }' > ./site/private/authConfig.json # - npm run fix - npm run build - npm start & - sleep 10 # Attempt to collect output for 10 seconds ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2017-2020 LBRY Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Spee.ch spee.ch provides a user-friendly, custom-designed, image and video hosting site backed by a decentralized network and blockchain ([LBRY](https://lbry.tech/)). Via just a small set of config files, you can spin your an entire spee.ch site back up including assets. **Please note: the spee.ch code base and setup instructions are no longer actively maintained now that we have lbry.tv. Proceed at your own caution. Setup will require dev ops skills.** ![App GIF](https://spee.ch/e/speechgif.gif) For a completely open, unrestricted example of a spee.ch site, check out https://www.spee.ch. For a closed, custom-hosted and branded example, check out https://lbry.theantimedia.com/. ## Installation ### Ubuntu Step-by-Step [Step-by-step Ubuntu Install Guide](./docs/ubuntuinstall.md) ### Full Instructions #### Get some information ready: - mysqlusername - mysqlpassword - domainname or 'http://localhost:3000' - speechport = 3000 #### Install and Set Up Dependencies - Firewall open ports - 22 - 80 - 443 - 3333 - 4444 - [NodeJS](https://nodejs.org) - [MySQL version 5.7 or higher](https://dev.mysql.com/doc/refman/8.0/en/installing.html) - mysqlusername or root - mysqlpassword - Requires mysql_native_password plugin ``` mysql> `ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'yourpassword';` ``` - [lbrynet](https://github.com/lbryio/lbry) daemon - run this as a service exposing ports 3333 and 4444 - _note_: once the daemon is running, issue commands in another terminal session (tmux) to retrieve an address for your wallet to recieve 5+ LBC credits (or join us in the [#speech discord channel](https://discord.gg/YjYbwhS) and we will send you a few) - `./lbrynet commands` gets a list of commands - `./lbrynet account_balance` gets your balance (initially 0.0) - `./lbrynet address_list` gets addresses you can use to recieve LBC - [FFmpeg](https://www.ffmpeg.org/download.html) - [ImageMagick](https://packages.ubuntu.com/xenial/graphics/imagemagick) - Spee.ch (below) - pm2 (optional) process manager such as pm2 to run speech server.js - http proxy server e.g. caddy, nginx, or traefik, to forward 80/443 to speech port 3000 - _note: even running on http://localhost, you must redirect http or https to port 3000_ #### Clone spee.ch - release version for stable production ``` $ git clone -b release https://github.com/lbryio/spee.ch.git ``` - master version for development ``` $ git clone https://github.com/lbryio/spee.ch.git ``` - your own fork for customization #### Change directory into your project ``` $ cd spee.ch ``` #### Install node dependencies ``` $ npm install ``` #### Create the config files using the built-in CLI Make sure lbrynet is running in the background before proceeding. _note: If you are opt to run a local chainquery, such as from [lbry-docker/chainquery](https://github.com/lbryio/lbry-docker/tree/master/chainquery) you will need to specify connection details at this time in:_ ~/spee.ch/docs/setup/conf/speech/chainqueryConfig.json ``` $ npm run configure ``` #### Build & start the app ``` $ npm run build $ npm run start ``` #### View in browser - Visit [http://localhost:3000](http://localhost:3000) in your browser #### Customize your app Check out the [customization guide](https://github.com/lbryio/spee.ch/blob/master/customize.md) to change your app's appearance and components #### (optional) add custom components and update the styles - Create custom components by creating React components in `site/custom/src/` - Update or override the CSS by changing the files in `site/custom/scss` #### (optional) install your own chainquery Instructions are coming at [lbry-docker] to install your own chainquery instance using docker-compose. This will require 50GB of preferably SSD space and at least 10 minutes to download, possibly much longer. ## Settings There are a number of settings available for customizing the behavior of your installation. [Here](https://github.com/lbryio/spee.ch/blob/master/docs/settings.md) is some documentation on them. ## API #### /api/claim/publish method: `POST` example: ``` curl -F 'name=MyPictureName' -F 'file=@/path/to/myPicture.jpeg' https://spee.ch/api/claim/publish ``` Parameters: - `name` (required, must be unique across the instance) - `file` (required) (must be type .mp4, .jpeg, .jpg, .gif, or .png) - `nsfw` (optional) - `license` (optional) - `title` (optional) - `description` (optional) - `thumbnail` URL to thumbnail image, for .mp4 uploads only (optional) - `channelName` channel to publish too (optional) - `channelPassword` password for channel to publish too (optional, but required if `channelName` is provided) response: ``` { "success": , "message": , "data": { "name": , "claimId": , "url": , "showUrl": , "serveUrl": , "lbryTx": { "claim_address": , "claim_id": , "fee": , "nout": , "tx": , "value": } } } ``` #### /api/claim/availability/:name method: `GET` example: ``` curl https://spee.ch/api/claim/availability/doitlive ``` response: ``` { "success": , // `true` if spee.ch successfully checked the claim availability "data": , // `true` if claim is available, false if it is not available "message": // human readable message of whether claim was available or not } ``` ## Contribute ### Stack The spee.ch stack is MySQL, Express.js, Node.js, and React.js. Spee.ch also runs `lbrynet` on its server, and it uses the `lbrynet` API to make requests -- such as `publish`, `create_channel`, and `get` -- on the `LBRY` network. Spee.ch also runs a sync tool, which decodes blocks from the `LBRY` blockchain as they are mined, and stores the information in MySQL. It stores all claims in the `Claims` table, and all channel claims in the `Certificates` table. - server - [MySQL](https://www.mysql.com/) - [express](https://www.npmjs.com/package/express) - [node](https://nodejs.org/) - [lbry](https://github.com/lbryio/lbry) - [FFmpeg](https://www.ffmpeg.org/) - client - [react](https://reactjs.org/) - redux - sagas - scss - handlebars ### Architecture - `cli/` contains the code for the CLI tool. Running the tool will create `.json` config files and place them in the `site/config/` folder - `configure.js` is the entry point for the CLI tool - `cli/defaults/` holds default config files - `cli/questions/` holds the questions that the CLI tool asks to build the config files - `client/` contains all of the client code - The client side of spee.ch uses `React` and `Redux` - `client/src/index.js` is the entry point for the client side js. It checks for preloaded state, creates the store, and places the `` component in the document. - `client/src/app.js` holds the `` component, which contains the routes for `react-router-dom` - `client/src/` contains all of the JSX code for the app. When the app is built, the content of this folder is transpiled into the `client/build/` folder. - The Redux code is broken up into `actions/` `reducers/` and `selectors/` - The React components are broken up into `containers/` (components that pull props directly from the Redux store), `components/` ('dumb' components), and `pages/` - spee.ch also uses sagas which are in the `sagas/` folders and `channels/` - `client/scss/` contains the CSS for the project \* - `site/custom` is a folder which can be used to override the default components in `client/` - The folder structure mimics that of the `client/` folder - to customize spee.ch, place your own components and scss in the `site/custom/src/` and `site/custom/scss` folders. - `server/` contains all of the server code - `index.js` is the entry point for the server. It creates the [express app](https://expressjs.com/), requires the routes, syncs the database, and starts the server listening on the `PORT` designated in the config files. - `server/routes/` contains all of the routes for the express app - `server/controllers/` contains all of the controllers for all of the routes - `server/models/` contains all of the models which the app uses to interact with the `MySQL` database. - Spee.ch uses the [sequelize](http://docs.sequelizejs.com/) ORM for communicating with the database. - `tests/` holds the end-to-end tests for this project - Spee.ch uses `mocha` with the `chai` assertion library - unit tests are located inside the project in-line with the files being tested and are designated with a `xxxx.test.js` file name ### Tests - This package uses `mocha` with `chai` for testing. - Before running tests, create a `testingConfig.js` file in `devConfig/` by copying `testingConfig.example.js` - To run tests: - To run all tests, including those that require LBC (like publishing), simply run `npm test` - To run only tests that do not require LBC, run `npm run test:no-lbc` ### URL formats Spee.ch has a few types of URL formats that return different assets from the LBRY network. Below is a list of all possible URLs for the content on spee.ch. You can learn more about LBRY URLs [here](https://lbry.tech/resources/uri). - retrieve the controlling `LBRY` claim: - https://spee.ch/`claim` - https://spee.ch/`claim`.`ext` (serve) - https://spee.ch/`claim`.`ext`&`querystring` (serve transformed) - retrieve a specific `LBRY` claim: - https://spee.ch/`claim_id`/`claim` - https://spee.ch/`claim_id`/`claim`.`ext` (serve) - https://spee.ch/`claim_id`/`claim`.`ext`&`querystring` (serve transformed) - retrieve all contents for the controlling `LBRY` channel - https://spee.ch/`@channel` - a specific `LBRY` channel - https://spee.ch/`@channel`:`channel_id` - retrieve a specific claim within the controlling `LBRY` channel - https://spee.ch/`@channel`/`claim` - https://spee.ch/`@channel`/`claim`.`ext` (serve) - https://spee.ch/`@channel`/`claim`.`ext`&`querystring` (serve) - retrieve a specific claim within a specific `LBRY` channel - https://spee.ch/`@channel`:`channel_id`/`claim` - https://spee.ch/`@channel`:`channel_id`/`claim`.`ext` (serve) - https://spee.ch/`@channel`:`channel_id`/`claim`.`ext`&`querystring` (serve) - `querystring` can include the following transformation values separated by `&` - h=`number` (defines height) - w=`number` (defines width) - t=`crop` or `stretch` (defines transformation - missing implies constrained proportions) ### Dependencies Spee.ch depends on two other lbry technologies: - [chainquery](https://github.com/lbryio/chainquery) - a normalized database of the blockchain data. We've provided credentials to use a public chainquery service. You can also install it on your own server to avoid being affected by the commons. - [lbrynet](https://github.com/lbryio/lbry) - a daemon that handles your wallet and transactions. ### Bugs If you find a bug or experience a problem, please report your issue here on GitHub and find us in the lbry discord! ## License This project is MIT licensed. For the full license, see [LICENSE](LICENSE). ## Security We take security seriously. Please contact security@lbry.com regarding any security issues. [Our GPG key is here](https://lbry.com/faq/gpg-key) if you need it. ## Contact The primary contact for this project is [@jessopb](mailto:jessop@lbry.com). ================================================ FILE: changelog.md ================================================ ================================================ FILE: cli/configure.js ================================================ const inquirer = require('inquirer'); const fs = require('fs'); const Path = require('path'); const axios = require('axios'); const ip = require('ip'); const pwGenerator = require('generate-password'); const mysqlQuestions = require(Path.resolve(__dirname, 'questions/mysqlQuestions.js')); const siteQuestions = require(Path.resolve(__dirname, 'questions/siteQuestions.js')); let primaryClaimAddress = ''; let thumbnailChannelDefault = '@thumbnails'; let thumbnailChannel = ''; let thumbnailChannelId = ''; const createConfigFile = (fileName, configObject, topSecret) => { // siteConfig.json , siteConfig const fileLocation = topSecret ? Path.resolve(__dirname, `../site/private/${fileName}`) : Path.resolve(__dirname, `../site/config/${fileName}`); const fileContents = JSON.stringify(configObject, null, 2); fs.writeFileSync(fileLocation, fileContents, 'utf-8'); console.log(`Successfully created ${fileLocation}\n`); }; // import existing configs or import the defaults let mysqlConfig; try { mysqlConfig = require('../site/config/mysqlConfig.json'); } catch (error) { mysqlConfig = require('./defaults/mysqlConfig.json'); } const { database: mysqlDatabase, username: mysqlUsername, password: mysqlPassword } = mysqlConfig; let siteConfig; try { siteConfig = require('../site/config/siteConfig.json'); } catch (error) { siteConfig = require('./defaults/siteConfig.json'); } const { details: { port, title, host }, publishing: { uploadDirectory, channelClaimBidAmount: channelBid }, } = siteConfig; let lbryConfig; try { lbryConfig = require('../site/config/lbryConfig.json'); } catch (error) { lbryConfig = require('./defaults/lbryConfig.json'); } let loggerConfig; try { loggerConfig = require('../site/config/loggerConfig.json'); } catch (error) { loggerConfig = require('./defaults/loggerConfig.json'); } let slackConfig; try { slackConfig = require('../site/config/slackConfig.json'); } catch (error) { slackConfig = require('./defaults/slackConfig.json'); } let chainqueryConfig; try { chainqueryConfig = require('../site/config/chainqueryConfig.json'); } catch (error) { chainqueryConfig = require('./defaults/chainqueryConfig.json'); } // authConfig let randSessionKey = pwGenerator.generate({ length: 20, numbers: true, }); let randMasterPass = pwGenerator.generate({ length: 20, numbers: true, }); let authConfig; try { authConfig = require('../site/private/authConfig.json'); } catch (error) { authConfig = { sessionKey: randSessionKey, masterPassword: randMasterPass, }; } // ask user questions and create config files inquirer .prompt(mysqlQuestions(mysqlDatabase, mysqlUsername, mysqlPassword)) .then(results => { console.log('\nCreating mysql config file...'); createConfigFile('mysqlConfig.json', results); }) .then(() => { // check for lbrynet connection & retrieve a default address console.log('\nRetrieving your primary claim address from LBRY...'); return axios .post('http://localhost:5279', { method: 'address_list', }) .then(response => { if (response.data) { if (response.data.error) { throw new Error(response.data.error.message); } primaryClaimAddress = response.data.result[0]; console.log('Primary claim address:', primaryClaimAddress, '!\n'); siteConfig['publishing']['primaryClaimAddress'] = primaryClaimAddress; return; } throw new Error('No data received from lbrynet'); }) .catch(error => { throw error; }); }) .then(() => { console.log('\nChecking to see if a LBRY channel exists for thumbnails...'); // see if a channel name already exists in the configs const { publishing } = siteConfig; thumbnailChannel = publishing.thumbnailChannel; thumbnailChannelId = publishing.thumbnailChannelId; console.log(`Thumbnail channel in configs: ${thumbnailChannel}#${thumbnailChannelId}.`); // see if channel exists in the wallet return axios .post('http://localhost:5279', { method: 'channel_list', }) .then(response => { if (response.data) { if (response.data.error) { throw new Error(response.data.error.message); } const channelList = response.data.result || []; console.log('channels in this wallet:', channelList.length); for (let i = 0; i < channelList.length; i++) { if (channelList[i].name === thumbnailChannelDefault) { const foundChannel = channelList[i]; console.log(`Found a thumbnail channel in wallet.`); if (foundChannel.is_mine === false) { console.log('Channel was not mine'); continue; } console.log('name:', foundChannel.name); console.log('claim_id:', foundChannel.claim_id); if ( foundChannel.name === thumbnailChannel && foundChannel.claim_id === thumbnailChannelId ) { console.log('No update to existing thumbnail config required\n'); } else { console.log(`Replacing thumbnail channel in config...`); siteConfig['publishing']['thumbnailChannel'] = foundChannel.name; siteConfig['publishing']['thumbnailChannelId'] = foundChannel.claim_id; console.log(`Successfully replaced channel in config.\n`); } return true; } } console.log(`Did not find a thumbnail channel that is mine in wallet.\n`); return false; } throw new Error('No data received from lbrynet'); }) .catch(error => { throw error; }); }) .then(thumbnailChannelAlreadyExists => { // exit if a channel already exists, skip this step if (thumbnailChannelAlreadyExists) { return; } // create thumbnail address console.log('\nCreating a LBRY channel to publish your thumbnails to...'); return axios .post('http://localhost:5279', { method: 'channel_new', params: { name: thumbnailChannelDefault, bid: channelBid, }, }) .then(response => { if (response.data) { if (response.data.error) { throw new Error(response.data.error.message); } thumbnailChannel = thumbnailChannelDefault; thumbnailChannelId = response.data.result.claim_id; siteConfig['publishing']['thumbnailChannel'] = thumbnailChannel; siteConfig['publishing']['thumbnailChannelId'] = thumbnailChannelId; console.log(`Created channel: ${thumbnailChannel}#${thumbnailChannelId}\n`); return; } throw new Error('No data received from lbrynet'); }) .catch(error => { throw error; }); }) .then(() => { return inquirer.prompt(siteQuestions(port, title, host, uploadDirectory)); }) .then(results => { console.log('\nCreating site config file...'); siteConfig['details']['port'] = results.port; siteConfig['details']['title'] = results.title; siteConfig['details']['host'] = results.host; siteConfig['details']['ipAddress'] = ip.address(); siteConfig['publishing']['uploadDirectory'] = results.uploadDirectory; }) .then(() => { // create the config files createConfigFile('siteConfig.json', siteConfig); createConfigFile('lbryConfig.json', lbryConfig); createConfigFile('loggerConfig.json', loggerConfig); createConfigFile('slackConfig.json', slackConfig); createConfigFile('chainqueryConfig.json', chainqueryConfig); createConfigFile('authConfig.json', authConfig, true); }) .then(() => { console.log("\nYou're all done!"); console.log( '\nIt\'s a good idea to BACK UP YOUR MASTER PASSWORD \nin "/site/private/authConfig.json" so that you don\'t lose \ncontrol of your channel.' ); console.log( '\nNext step: run "npm run build" (or "npm run dev") to compiles, and "npm run start" to start your server!' ); console.log( 'If you want to change any settings, you can edit the files in the "/site" folder.' ); process.exit(0); }) .catch(error => { if (error.code === 'ECONNREFUSED') { console.log('Error: could not connect to LBRY. Please make sure lbrynet daemon is running.'); process.exit(1); } else { console.log(error); process.exit(1); } }); ================================================ FILE: cli/defaults/chainqueryConfig.json ================================================ { "host": "public.chainquery.lbry.com", "port": "3306", "timeout": 30, "database": "chainquery", "username": "speechpublic", "password": "7uITJLwZRvHBZYS3JZDykD1-7hLVkVA1jDWfcgqi6QnC" } ================================================ FILE: cli/defaults/lbryConfig.json ================================================ { "apiHost": "localhost", "apiPort": "5279", "getTimeout": 30 } ================================================ FILE: cli/defaults/loggerConfig.json ================================================ { "logLevel": "verbose" } ================================================ FILE: cli/defaults/mysqlConfig.json ================================================ { "database": "lbry", "username": "root", "password": "" } ================================================ FILE: cli/defaults/siteConfig.json ================================================ { "analytics": { "googleId": null }, "assetDefaults": { "title": "Default Content Title", "description": "Default Content Description", "thumbnail": "https://spee.ch/0e5d4e8f4086e13f5b9ca3f9648f518e5f524402/speechflag.png" }, "auth": { "sessionKey": "mysecretkeyword", "masterPassword": "myMasterPassword" }, "details": { "port": 3000, "title": "My Site", "ipAddress": "", "host": "https://www.example.com", "description": "A decentralized hosting platform built on LBRY", "twitter": false, "blockListEndpoint": "https://api.lbry.com/file/list_blocked" }, "publishing": { "primaryClaimAddress": null, "uploadDirectory": "/home/lbry/Uploads", "thumbnailChannel": null, "thumbnailChannelId": null, "additionalClaimAddresses": [], "disabled": false, "disabledMessage": "Default publishing disabled message", "closedRegistration": false, "serveOnlyApproved": false, "publishOnlyApproved": false, "approvedChannels": [], "publishingChannelWhitelist": [], "channelClaimBidAmount": "0.1", "fileClaimBidAmount": "0.01", "fileSizeLimits": { "image": 50000000, "video": 50000000, "audio": 50000000, "text": 50000000, "model": 50000000, "application": 50000000, "customByContentType": { "application/octet-stream": 50000000 } } }, "serving": { "dynamicFileSizing": { "enabled": true, "maxDimension": 2000 }, "markdownSettings": { "skipHtmlMain": true, "escapeHtmlMain": true, "skipHtmlDescriptions": true, "escapeHtmlDescriptions": true, "allowedTypesMain": [], "allowedTypesDescriptions": [], "allowedTypesExample": [ "see react-markdown docs", "root", "text", "break", "paragraph", "emphasis", "strong", "thematicBreak", "blockquote", "delete", "link", "image", "linkReference", "imageReference", "table", "tableHead", "tableBody", "tableRow", "tableCell", "list", "listItem", "heading", "inlineCode", "code", "html", "parsedHtml" ] }, "customFileExtensions": { "application/x-troff-man": "man", "application/x-troff-me": "me", "application/x-mif": "mif", "application/x-troff-ms": "ms", "application/x-troff": "roff", "application/x-python-code": "pyc", "text/x-python": "py", "application/x-pn-realaudio": "ram", "application/x-sgml": "sgm", "model/stl": "stl", "image/pict": "pct", "text/xul": "xul", "text/x-go": "go" } }, "startup": { "performChecks": true, "performUpdates": true } } ================================================ FILE: cli/defaults/slackConfig.json ================================================ { "slackWebHook": false, "slackErrorChannel": false, "slackInfoChannel": false } ================================================ FILE: cli/questions/mysqlQuestions.js ================================================ const database = (defaultAnswer) => { return { type : 'input', message: 'What is the name of the MySQL DATABASE to be used?', default: defaultAnswer, name : 'database', }; }; const username = (defaultAnswer) => { return { type : 'input', message: 'What is the USER NAME for your MySQL database?', default: defaultAnswer, name : 'username', }; }; const password = (defaultAnswer) => { return { type : 'input', message: 'What is the PASSWORD for your MySQL database?', default: defaultAnswer, name : 'password', }; }; module.exports = (defaultDatabase, defaultUsername, defaultPassword) => { return [ database(defaultDatabase), username(defaultUsername), password(defaultPassword), ]; }; ================================================ FILE: cli/questions/siteQuestions.js ================================================ const makeDir = require('make-dir'); const port = (defaultAnswer) => { return { type : 'input', message: 'Enter a PORT for your server to run on.', default: defaultAnswer, name : 'port', }; }; const title = (defaultAnswer) => { return { type : 'input', message: 'Enter a title for your site.', default: defaultAnswer, name : 'title', }; }; const host = (defaultAnswer) => { return { type : 'input', message: 'Enter your site\'s domain.', default: defaultAnswer, name : 'host', }; }; const uploadDirectory = (defaultAnswer) => { return { type : 'input', message: 'Enter a directory where uploads should be stored.', default: defaultAnswer, name : 'uploadDirectory', validate (input) { // make sure the directory exists return new Promise((resolve, reject) => { console.log('\n\nCreating directory', input, '...'); try { const dirPath = makeDir.sync(input); console.log('Successfully created directory at', dirPath, '\n'); } catch (error) { console.log('Failed to create directory, please create directory manually.\n'); } resolve(true); }); }, }; }; module.exports = (defaultPort, defaultTitle, defaultHost, defaultUploadDirectory) => { return [ port(defaultPort), title(defaultTitle), host(defaultHost), uploadDirectory(defaultUploadDirectory), ]; }; ================================================ FILE: client/scss/_asset-blocked.scss ================================================ .asset-blocked__image { width: 100%; } .asset-blocked__text { width: 100%; } ================================================ FILE: client/scss/_asset-display.scss ================================================ .asset-main { display: flex; flex-direction: column; align-items: center; } .asset-document { max-width: 1000px; padding: $thin-padding; height: fit-content; box-sizing: border-box; } .asset-display { height: fit-content; width: fit-content; } .asset-title { max-width: 1000px; padding-bottom: $thin-padding; text-align: center; } .asset-image, .asset-video { max-height: 75vh; max-width: 85vw; object-fit: contain; object-position: center; background: black; } /*below must die if this is intended to be shareable component! it also probably doesn't need to be*/ .visible-content { margin: 0; padding-bottom: 30px; position: relative; width: 100%; &.closed { box-shadow: none; &:after { box-shadow: none; } } &:after { box-shadow: 0px 2px 3px 2px $shadow-color; content: ''; height: 0; position: absolute; top: 100%; width: 100%; z-index: 100; } } .vertical-split, .visible-content { flex : 1 0 auto; display : flex; flex-direction : column; justify-content: space-between; align-items : center; }; .collapse-content { flex-grow: 0; @media (max-width: $break-point-tablet) { max-width: 100%; width: 100%; } } .collapse-content.closed{ display: none; } .collapse-button { background: none; border: none; display: block; margin: 15px auto 0; width: 25px; height: 25px; text-align: center; padding: 0px; @media (max-width: $break-point-tablet) { padding: 0; } svg { stroke: $primary-color; &.plus-icon { transform: rotate(0); transition: all 0.4s ease; } } &:hover { .plus-icon { transform: rotate(-180deg); } } } .asset-info { $asset-info-width: 1000px; max-width: $asset-info-width; margin: $primary-padding; width: 100%; @media (min-width: $break-point-tablet) { padding: $primary-padding; } @media (max-width: $break-point-tablet) { padding: $tertiary-padding; } @media (max-width: $break-point-mobile) { margin: $tertiary-padding; } } .asset-footer { border-top: $subtle-border; padding-top: $primary-padding; margin-top: $primary-padding; color: $grey; text-align: center; } ================================================ FILE: client/scss/_asset-preview.scss ================================================ .asset-preview { display: flex; flex-direction: column; background: $card-color; color: $text-color; width: 240px; border: $subtle-border; height: 256px; &:hover { border-color: $highlight-border-color; color: $primary-color; } } .asset-preview__image { height : 180px; width : 240px; overflow: hidden; object-fit: cover; padding: 0; margin : 0; box-sizing: border-box; } .asset-preview__image-box { width : 240px; height : 180px; padding: 0; margin : 0; box-sizing: border-box; } .asset-preview__label { padding: $thin-padding; display: flex; flex-direction: column; justify-content: space-between; } .asset-preview__label-text { overflow: hidden; display: flex; flex-direction: column; justify-content: space-around; box-sizing: border-box; font-size: $text-small; font-weight: bold; height: 54px; } .asset-preview__label-info { width: 100%; height: 15px; display: flex; flex-direction: row; justify-content: space-between; overflow: hidden; text-overflow: ellipsis; box-sizing: border-box; align-items: center; } .asset-preview__label-info-datum { display: flex; flex-direction: row; align-items: center; overflow: hidden; box-sizing: border-box; font-size: $text-small; max-width: 40%; } .asset-preview__label-info-datum svg{ height: 1.2em; width: 1.2em; padding: 0; padding-right: $thin-padding; margin: 0; } .asset-preview__label-info-datum .svg-icon{ padding: 0px; margin: 0; height: 15px; } .asset-preview__blocked { box-sizing: border-box; background: black; color: white; height: 64%; padding: $thin-padding; margin-bottom: $thin-padding; } ================================================ FILE: client/scss/_body.scss ================================================ body { margin: 0; padding: 0; min-height: 100%; word-wrap: break-word; display: -webkit-flex; display: flex; -webkit-flex-direction: column; flex-direction: column; } ================================================ FILE: client/scss/_button-primary.scss ================================================ .button--primary, .button--primary:focus, .button--primary:active { border-color: $primary-color; color: $primary-color; background-color: $background-color; } .button--primary:hover { color: $background-color; background-color: $primary-color; } .button--primary:active { $color: darken($primary-color, 10%); border-color: $color; background-color: $color; } ================================================ FILE: client/scss/_button-secondary.scss ================================================ .button--secondary, .button--secondary:focus, .button--secondary:active { border-bottom-color: $secondary-color; color: $secondary-color; background-color: $background-color; } .button--secondary:active { $color: darken($secondary-color, 10%); color: $color; border-bottom-color: $color; } ================================================ FILE: client/scss/_button.scss ================================================ button { cursor: pointer; &:active { outline: 0; } } .button--primary, .button--secondary { border-width: $button-border-width; border-style: $button-border-strength; border-color: transparent; padding: $thin-padding; } .button--jumbo, .button--jumbo:focus, .button--jumbo:active { width: $button-full-width; font-size: x-large; } ================================================ FILE: client/scss/_channel-claims-display.scss ================================================ .channel-claims-display { width: 100%; display: grid; grid-gap: $tertiary-padding; align-content: space-around; @media (min-width: $break-point-x-large) { grid-template-columns: 1fr 1fr 1fr 1fr 1fr; } @media (min-width: $break-point-large) and (max-width: $break-point-x-large){ grid-template-columns: 1fr 1fr 1fr 1fr; } @media (min-width: $break-point-tablet) and (max-width: $break-point-large) { grid-template-columns: 1fr 1fr 1fr; } @media (min-width: $break-point-mobile) and (max-width: $break-point-tablet) { grid-template-columns: 1fr 1fr; } @media (max-width: $break-point-mobile) { grid-template-columns: 1fr; } } ================================================ FILE: client/scss/_claim-pending.scss ================================================ .claim-pending { display: inline-block; position: absolute; top: 10px; left: 10px; padding: 5px; border-radius: 5px; border: 2px solid red; color: red; font-weight: bold; background-color: white; } ================================================ FILE: client/scss/_click-to-copy.scss ================================================ .click-to-copy-wrap { display: flex; width: 100%; justify-content: space-between; align-items: center; cursor: pointer; border: 1px solid $subtle-border-color; border-radius: 6px; .click-to-copy { border: none; padding: 0.36em 0.5em; margin: 0; background-color: transparent; width: calc(100% - 1em - 2px); font-size: 14px; letter-spacing: -0.6px; line-height: 20px; letter-spacing: 0; font-family: monospace; border-right: 1px solid $subtle-border-color; } .icon-wrap { width: 30px; height: 30px; line-height: 34px; text-align: center; svg { stroke: $primary-color; width: 16px; height: 16px; } } } ================================================ FILE: client/scss/_dropzone.scss ================================================ .dropzone-wrapper { // fill the parent flex container flex: 1 0 auto; // be a flex container for children display: flex; flex-direction: column; position: relative; width: 100%; box-sizing: border-box; padding: 0px; } .dropzone { border: 2px dashed $drop-zone-border-color; // fill the parent flex container flex: 1 0 auto; // be a flex container for children display: flex; padding: $thin-padding; -webkit-flex-direction: column; flex-direction: column; justify-content: center; align-items: center; user-select: none; } .dropzone:hover, .dropzone--active { border: 2px dashed $drop-zone-border-hover; cursor: pointer; } .dropzone-dropit-display, .dropzone-instructions-display { padding: 1em; text-align: center; } .dropzone-dropit-display { color: $primary-color; } .dropzone-preview-wrapper { position: relative; width: 100%; .dropzone-preview-overlay { position: absolute; left: 0; top: 0; width: 100%; height: 100%; display: flex; -webkit-flex-direction: column; flex-direction: column; justify-content: center; } } .dropzone-preview-image { display: block; width: 100%; } .dropzone-preview-memeify { margin-top: 3em; } .dropzone-memeify-button { background: $primary-color; color: #fff; cursor: pointer; font-size: .8em; padding: 3px 6px; position: absolute; right: 0; top: 0; z-index: 3; } .dropzone-memeify-saveMessage { padding-top: .25em; position: relative; top: .5em; } .dropzone-memeify-toolbar { /* TODO: Cleanup `!important` */ background: $primary-color !important; left: -1em !important; right: -1em !important; top: -4em !important; } .dropzone-instructions-display__chooser-label { text-decoration: underline; } ================================================ FILE: client/scss/_form-feedback.scss ================================================ .form-feedback { padding-top: $thin-padding; padding-bottom: $thin-padding; } .form-feedback--default { color: $secondary-color; } .form-feedback--success { color: $success-color; } .form-feedback--failure { color: $failure-color; } ================================================ FILE: client/scss/_form.scss ================================================ .form-group { padding-bottom: $secondary-padding; } .form-title { padding-bottom: $secondary-padding; } ================================================ FILE: client/scss/_horizontal-split.scss ================================================ .horizontal-split { max-width: $width-content-constrained; width: 100%; display : flex; flex-direction : row; justify-content: center; box-sizing: border-box; &.horizontal-split--mobile-collapse { @media (max-width: $break-point-tablet) { flex-direction: column; .horizontal-split__column { } .horizontal-split__column--left { padding-top: $thin-padding; } .horizontal-split__column--right { padding-top: $thin-padding; } } } }; .horizontal-split__column { display : flex; flex: 1 1 auto; box-sizing: border-box; width: 100%; } .horizontal-split__column--left { padding: $tertiary-padding; @media (max-width: $break-point-tablet) { padding-left: 0px; padding-right: 0px; } } .horizontal-split__column--right { padding: $tertiary-padding; @media (max-width: $break-point-tablet) { padding-left: 0px; padding-right: 0px; } } @media (max-width: $break-point-tablet) { .horizontal-split__column { justify-content: space-between; }; .column { width: 100%; padding-left: 0; padding-right: 0; padding-bottom: $secondary-padding; } } ================================================ FILE: client/scss/_html.scss ================================================ html { margin: 0; padding: 0; height: 100%; } ================================================ FILE: client/scss/_input.scss ================================================ input:-webkit-autofill { -webkit-box-shadow: 0 0 0px 1000px white inset; } input { margin: 0; padding: $input-padding; border: 0; background-color: $background-color; display: inline-block; color: $text-color } .input-slider { width: 100% } .input-checkbox { border: 1px solid black; background: white; } .input-file { width: 0.1px; height: 0.1px; opacity: 0; overflow: hidden; position: absolute; z-index: -1; } .input-radio { cursor: pointer; } // input area wrapper .input-area { border-bottom: 1px solid $secondary-color; } .form-group { padding-bottom: $secondary-padding; } // modifiers .input--full-width { width: $input-full-width; } ================================================ FILE: client/scss/_label.scss ================================================ .label { padding-top: $thin-padding; padding-bottom: $thin-padding; display: inline-block; font-weight: bold; font-size: $text-medium; width: 100%; box-sizing: border-box; } .label-radio { padding-left: $thin-padding; padding-right: $thin-padding; cursor: pointer; font-weight: bold; } @media (max-width: $break-point-tablet ) { // note: bolding break point lines up with row-label break point .label, .label-radio { font-weight: bold; } } ================================================ FILE: client/scss/_link.scss ================================================ a, a:visited { text-decoration: none; } .link--primary, .link--primary:visited { color: $primary-color; &:hover { text-decoration: underline; } } .link--nav { color: $text-color; &:hover { color: $primary-color; } } .link--nav-active { border-bottom: 2px solid $primary-color; } ================================================ FILE: client/scss/_markdown.scss ================================================ .markdown-preview { margin: $tertiary-padding 0px; h1, h2, h3 { font-size: inherit; font-weight: inherit; margin: $tertiary-padding 0px; } h4, h5, h6 { font-size: $text-large; font-weight: 600; margin: $tertiary-padding 0px; } // Paragraphs p { font-size: 1.15rem; white-space: pre-line; margin: $tertiary-padding 0px; svg { width: 1rem; height: 1rem; margin-left: 0.2rem; position: relative; top: 1px; } } blockquote { background: $blockquote-background; padding: $tertiary-padding; min-width: 60%; margin: $tertiary-padding; p:first-child{ margin-top: 0px; } p:last-child { margin-bottom: 0px; } div { display: none; } } // Strikethrough text del { } // Tables table { width: 100%; background-color: $base-color; border-spacing: 0; border: .5px solid $chrome-color; margin: $tertiary-padding 0px; tr { td, th, td:first-of-type, th:first-of-type, td:last-of-type, th:last-of-type { padding: $thin-padding $tertiary-padding; text-overflow: ellipsis; } td:last-of-type { text-align: right; } th { background: $chrome-color; } } tr:nth-child(even){ background: $chrome-color; } } // Image img { margin-bottom: $tertiary-padding; margin-top: $tertiary-padding; padding: $secondary-padding; object-fit: scale-down; box-sizing: border-box; display: block; margin-left: auto; margin-right: auto; max-width: 90vw; } iframe { display: block; margin-left: auto; margin-right: auto; max-width: 90vw; margin-top: $tertiary-padding 0px; margin-bottom: $tertiary-padding 0px; } // Horizontal Rule hr { width: 100%; height: 1px; background-color: $base-color; margin-bottom: 2rem; position: relative; top: 1rem; html[data-theme='dark'] & { background-color: rgba($base-color, 0.2); } } // Code pre { white-space: normal; } code { margin-bottom: $tertiary-padding; padding: $tertiary-padding; background-color: $subtle-border-color; color: $text-color; display: block; font-family: Consolas, 'Lucida Console', 'Source Sans', monospace; } a { color: $primary-color; display: inline; } // Lists ul, ol { margin-bottom: $thin-padding; > li { list-style-position: outside; } } ul { list-style: initial; } li { margin-left: $primary-padding; p { display: inline-block; } } } ================================================ FILE: client/scss/_media-queries.scss ================================================ @media (max-width: $break-point-x-large) { // hide site description in nav bar .site-description { display: none; } } ================================================ FILE: client/scss/_nav-bar.scss ================================================ .nav-bar { box-sizing: border-box; padding: $thin-padding $primary-padding; background: $chrome-color; flex: 0 1 auto; width: 100%; border-bottom: $subtle-border; color: $primary-color; @media (max-width: $break-point-mobile) { margin-left: 15px; margin-right: 15px; } input { background: $chrome-color; } select { background: $chrome-color; color: $text-color; } } .nav-bar-link { padding: calc(1em - 2px); display: inline-block; font-size: $text-medium; letter-spacing: 0.4px; text-transform: uppercase; } .nav-bar-logo { cursor: pointer; } @media (max-width: $break-point-tablet ) { .nav-bar-link { padding-top: calc(1em - 2px); padding-right: 1em; padding-bottom: calc(1em - 2px); padding-left: 1em; } } @media (max-width: $break-point-mobile ) { .nav-bar-link { padding-top: calc(0.5em - 2px); padding-right: 0.5em; padding-bottom: calc(0.5em - 2px); padding-left: 0.5em; } } ================================================ FILE: client/scss/_page-content.scss ================================================ .page-content { margin: $primary-padding; // fill the parent flex container flex: 1 0 auto; // be a flex container for children display: flex; -webkit-flex-direction: column; flex-direction: column; }; ================================================ FILE: client/scss/_page-layout-show-lite.scss ================================================ .page-layout-show-lite { flex: 1 0 auto; display: flex; flex-direction: column; .content { flex: 1 0 auto; display: flex; flex-direction: column; } .footer { flex: 0 1 auto; } } ================================================ FILE: client/scss/_page-layout.scss ================================================ .page-layout { flex: 1 0 auto; display: flex; flex-direction: column; align-items: center; max-width: 100%; .content { flex: 1 0 auto; display: flex; -webkit-flex-direction: column; flex-direction: column; width: 100%; align-items: center; box-sizing: border-box; background: $base-color; @media (min-width: $break-point-tablet) { padding: $primary-padding; } @media (max-width: $break-point-tablet) { padding: $tertiary-padding; } } } ================================================ FILE: client/scss/_progress-bar.scss ================================================ .progress-bar__wrapper { display: flex; align-items: center; justify-content: center; } .progress-bar--inactive { color: $grey; } .progress-bar--active { color: $primary-color; } ================================================ FILE: client/scss/_publish-disabled-message.scss ================================================ .publish-disabled-message { // fill the parent flex container flex: 1 0 auto; // be a flex container for children display: flex; flex-direction: column; justify-content: center; .message { text-align: center; } } ================================================ FILE: client/scss/_publish-preview.scss ================================================ .publish-form__title { max-width: $width-content-constrained; margin-left: auto; margin-right: auto; @media (max-width: $break-point-mobile) { font-size: .8em; } } .publish-preview-dim { opacity: 0.2; } ================================================ FILE: client/scss/_publish-status.scss ================================================ .publish-status { // fill the parent flex container flex: 1 0 auto; // be a flex container for children display: flex; flex-direction: column; justify-content: center; .status { text-align: center; } } ================================================ FILE: client/scss/_publish-url-input.scss ================================================ .publish-url-input { display: flex; flex-direction: row; flex-wrap: nowrap; justify-content: flex-start; align-items: baseline; border-bottom: solid 1px grey; .shrink { flex: 0 1 auto; }; .fill { flex: 1 0 auto; }; } .publish-url-text { margin: 0; padding: 0; color: $help-color; } ================================================ FILE: client/scss/_react-app.scss ================================================ #react-app { flex: 1 0 auto; display: -webkit-flex; display: flex; -webkit-flex-direction: column; flex-direction: column; } ================================================ FILE: client/scss/_reset.scss ================================================ button, input, textarea, label, select, option { font-family: inherit; font-size: inherit; } ================================================ FILE: client/scss/_row.scss ================================================ .row { margin-bottom: 1.2em; } .row-labeled { display: flex; flex-direction: column; flex-wrap: nowrap; justify-content: flex-start; padding-bottom: $tertiary-padding; } .row-labeled-label { width: 100%; display: flex; align-items: center; flex: 1; } .row-labeled-content { align-self: center; width: 100%; } @media (max-width: $break-point-tablet ) { .row-labeled { flex-direction: column; } .row-labeled-label { width: 100%; } .row-labeled-content { width: 100%; } } ================================================ FILE: client/scss/_select.scss ================================================ select { margin: 0; display: inline-block; background: $background-color; border: 0; color: $text-color; } ================================================ FILE: client/scss/_share-buttons.scss ================================================ .share-buttons { display: flex; align-items: center; a { display: block; width: 30px; height: 30px; margin: 0 7px; border-radius: 100%; line-height: 30px; text-align: center; transition: all 0.2s ease; &.twitter { background:#4DC2FE; img { margin-top: 8px; margin-left: 2px; } } &.facebook { background: #5487DE; img { margin-top: 6px; } } &.tumblr { background: #274061; img { margin-top: 7px; } } &.reddit { background: #FF4500; img { margin-top: 7px; } } &:first-child{ margin-left: 0px; } &:hover { background: $primary-color; } } } ================================================ FILE: client/scss/_social-share-link.scss ================================================ .social-share-link { flex-wrap: wrap; margin-right: -0.5em; margin-left: -0.5em; } .social-share-link > a{ padding-right:0.5em; padding-left:0.5em; } ================================================ FILE: client/scss/_space-around.scss ================================================ .space-around { display: flex; justify-content: space-around; align-items: center; } ================================================ FILE: client/scss/_space-between.scss ================================================ .space-between { display: flex; justify-content: space-between; align-items: center; } ================================================ FILE: client/scss/_text.scss ================================================ // set defaults h1, h2, h3, h4, p { margin: 0; } body { color: $text-color; font-family: 'Circular', serif; font-size: 14px; } body a { color: $primary-color; } h1 { font-size: $text-xx-large; } h2 { font-size: $text-x-large; } h3 { font-size: $text-large; } .text--extra-large { font-size: $text-xx-large; } .text--large { font-size: $text-large; } .text--medium { font-size: $text-medium; } .text--small { font-size: $text-small; } .text--extra-small { font-size: $text-x-small; } .text--secondary { color: $help-color; } .text--interactive { color: $primary-color; } .text--failure { color: $failure-color; } .text--success { color: $success-color; } ================================================ FILE: client/scss/_textarea.scss ================================================ textarea { margin: 0; padding: $input-padding; display: inline-block; width: $input-full-width; } ================================================ FILE: client/scss/_tooltip.scss ================================================ /* Tooltip container */ .tooltip { position: relative; } /* Tooltip text */ .tooltip > .tooltip-text { visibility: hidden; width: 15em; background-color: #9b9b9b; color: #fff; text-align: center; padding: 0.5em; /* Position the tooltip text */ position: absolute; z-index: 1; bottom: 110%; left: 50%; margin-left: -8em; /* Use half of the width (120/2 = 60), to center the tooltip */ } /* Show the tooltip text when you mouse over the tooltip container */ .tooltip:hover > .tooltip-text { visibility: visible; } /* arrow at bottom of tooltip text */ .tooltip > .tooltip-text::after { content: " "; position: absolute; top: 100%; left: 50%; margin-left: -5px; border-width: 5px; border-style: solid; border-color: #9b9b9b transparent transparent transparent; } ================================================ FILE: client/scss/_variables.scss ================================================ //backgrounds $base-color: white; //default white $card-color: white; //default white $chrome-color: white; //default white (navbar) $blockquote-background: #EEEEFF; $background-color: $base-color; //text colors $primary-color: #005da0; //link default light blue #005da0 $secondary-color: $primary-color; $text-color: #333; $success-color: green; $failure-color: red; $grey: #9095A5; $blockquote-text: $text-color; //borders and highlights $grey: #9095A5; $help-color: $grey; $subtle-border-color: #DDD; $highlight-border-color: #777; $shadow-color: rgba(169, 173, 186, 0.2); $subtle-border: 1px dashed $subtle-border-color; $grey-border: $subtle-border-color; //factor this out for all customers $drop-zone-border-color: #9b9b9b; //default #9b9b9b $drop-zone-border-hover: #4156C5; //default #4156C5 //padding $primary-padding: 3em; $secondary-padding: 2em; $tertiary-padding: 1em; $thin-padding: 0.3em; $full-width-thin-padding: calc(100% - 0.6em); $input-padding: 0.3em; $width-content-constrained: 1000px; $button-border-width: 1px; $button-border-strength: solid; $button-full-width: calc(100% - 2px); $input-full-width: calc(100% - 0.6em); //text sizes $base-font-size: 14px; $text-xx-large: 2.5em; $text-x-large: 2.0em; $text-large: 1.5em; $text-medium: 1.2em; $text-small: 0.9em; $text-x-small: 0.8em; //@media sizes $break-point-xx-large: 1400px; $break-point-x-large: 1290px; $break-point-large: 1024px; $break-point-tablet: 800px; $break-point-mobile: 500px; $break-point-phone: 300px; $break-point-phone: 300px; ================================================ FILE: client/scss/_video.scss ================================================ video:-moz-full-screen { border:none; padding:0; } video:-webkit-full-screen { border:none; padding:0; } video:fullscreen { border:none; padding:0; } ================================================ FILE: client/scss/all.scss ================================================ @import '~scss/_variables'; @import '~scss/_reset'; @import '~scss/font/_font.scss'; @import '~scss/_html'; @import '~scss/_body'; @import '~scss/_react-app'; @import '~scss/_text'; @import '~scss/_markdown'; @import '~scss/_link'; @import '~scss/_input'; @import '~scss/_select'; @import '~scss/_textarea'; @import '~scss/_video'; @import '~scss/_form'; @import '~scss/_asset-display'; @import '~scss/_asset-preview'; @import '~scss/_asset-blocked'; @import '~scss/_button'; @import '~scss/_button-primary'; @import '~scss/_button-secondary'; @import '~scss/_claim-pending'; @import '~scss/_click-to-copy'; @import '~scss/_form-feedback'; @import '~scss/_horizontal-split'; @import '~scss/_label'; @import '~scss/_nav-bar'; @import '~scss/_page-layout'; @import '~scss/_page-layout-show-lite'; @import '~scss/_page-content'; @import '~scss/_progress-bar'; @import '~scss/_publish-preview'; @import '~scss/_share-buttons'; @import '~scss/_space-between'; @import '~scss/_space-around'; @import '~scss/_row'; @import '~scss/_tooltip'; @import '~scss/_social-share-link'; @import '~scss/_channel-claims-display'; @import '~scss/_dropzone'; @import '~scss/_publish-url-input'; @import '~scss/_publish-status'; @import '~scss/_publish-disabled-message'; @import '~scss/_media-queries'; ================================================ FILE: client/scss/font/Lekton/OFL.txt ================================================ Copyright (c) 2008-2010, Isia Urbino (http://www.isiaurbino.net) This Font Software is licensed under the SIL Open Font License, Version 1.1. This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL ----------------------------------------------------------- SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ----------------------------------------------------------- PREAMBLE The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others. The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives. DEFINITIONS "Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation. "Reserved Font Name" refers to any names specified as such after the copyright statement(s). "Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s). "Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment. "Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software. PERMISSION & CONDITIONS Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions: 1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself. 2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user. 3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users. 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission. 5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software. TERMINATION This license becomes null and void if any of the above conditions are not met. DISCLAIMER THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. ================================================ FILE: client/scss/font/_font.scss ================================================ @font-face { font-family: 'Lekton'; src: url('./font/Lekton/Lekton-Regular.ttf'); } @font-face { font-family: 'Lekton'; src: url('./font/Lekton/Lekton-Bold.ttf'); font-weight: bold; font-style: normal; } @font-face { font-family: 'Lekton'; src: url('./font/Lekton/Lekton-Italic.ttf'); font-weight: normal; font-style: italic; } @font-face { font-family: 'Circular'; src: url('./font/Circular/CircularStd-Book.ttf'); font-weight: normal; } @font-face { font-family: 'Circular'; src: url('./font/Circular/CircularStd-Bold.ttf'); font-weight: bold; } ================================================ FILE: client/src/actions/channel.js ================================================ import * as actions from '../constants/channel_action_types'; // export action creators export function updateLoggedInChannel (name, shortId, longId) { return { type: actions.CHANNEL_UPDATE, data: { name, shortId, longId, }, }; } export function checkForLoggedInChannel () { return { type: actions.CHANNEL_LOGIN_CHECK, }; } export function logOutChannel () { return { type: actions.CHANNEL_LOGOUT, }; } ================================================ FILE: client/src/actions/channelCreate.js ================================================ import * as actions from '../constants/channel_create_action_types'; // export action creators export function updateChannelCreateName (name, value) { return { type: actions.CHANNEL_CREATE_UPDATE_NAME, data: { name, value, }, }; } export function updateChannelCreatePassword (name, value) { return { type: actions.CHANNEL_CREATE_UPDATE_PASSWORD, data: { name, value, }, }; } export function updateChannelCreateStatus (status) { return { type: actions.CHANNEL_CREATE_UPDATE_STATUS, data: status, }; } export function updateChannelAvailability (channel) { return { type: actions.CHANNEL_AVAILABILITY, data: channel, }; } export function createChannel () { return { type: actions.CHANNEL_CREATE, }; } ================================================ FILE: client/src/actions/index.js ================================================ // import { } from './channel'; // import { } from './publish'; import { onHandleShowPageUri } from './show'; export default { onHandleShowPageUri, }; ================================================ FILE: client/src/actions/publish.js ================================================ import * as actions from '../constants/publish_action_types'; // export action creators export function selectFile (file) { return { type: actions.FILE_SELECTED, data: file, }; } export function clearFile () { return { type: actions.FILE_CLEAR, }; } export function setUpdateTrue () { return { type: actions.SET_UPDATE_TRUE, }; } export function setHasChanged (status) { return { type: actions.SET_HAS_CHANGED, data: status, }; } export function updateMetadata (name, value) { return { type: actions.METADATA_UPDATE, data: { name, value, }, }; } export function updateClaim (value) { return { type: actions.CLAIM_UPDATE, data: value, }; }; export function abandonClaim (data) { return { type: actions.ABANDON_CLAIM, data, }; }; export function setPublishInChannel (channel) { return { type: actions.SET_PUBLISH_IN_CHANNEL, channel, }; } export function updatePublishStatus (status, message) { return { type: actions.PUBLISH_STATUS_UPDATE, data: { status, message, }, }; } export function updateError (name, value) { return { type: actions.ERROR_UPDATE, data: { name, value, }, }; } export function updateSelectedChannel (channelName) { return { type: actions.SELECTED_CHANNEL_UPDATE, data: channelName, }; } export function toggleMetadataInputs (showMetadataInputs) { return { type: actions.TOGGLE_METADATA_INPUTS, data: showMetadataInputs, }; } export function onNewThumbnail (file) { return { type: actions.THUMBNAIL_NEW, data: file, }; } export function startPublish (history) { return { type: actions.PUBLISH_START, data: { history }, }; } export function validateClaim (claim) { return { type: actions.CLAIM_AVAILABILITY, data: claim, }; } ================================================ FILE: client/src/actions/show.js ================================================ import * as actions from '../constants/show_action_types'; import { ASSET_DETAILS, ASSET_LITE, CHANNEL, SPECIAL_ASSET } from '../constants/show_request_types'; // basic request parsing export function onHandleShowPageUri(params, url) { return { type: actions.HANDLE_SHOW_URI, data: { ...params, url, }, }; } export function onHandleShowHomepage(params, url) { return { type: actions.HANDLE_SHOW_HOMEPAGE, data: { ...params, url, }, }; } export function onRequestError(error) { return { type: actions.REQUEST_ERROR, data: error, }; } export function onNewChannelRequest(channelName, channelId) { const requestType = CHANNEL; const requestId = `cr#${channelName}#${channelId}`; return { type: actions.CHANNEL_REQUEST_NEW, data: { requestType, requestId, channelName, channelId }, }; } export function onNewSpecialAssetRequest(name) { const requestType = SPECIAL_ASSET; const requestId = `sar#${name}`; return { type: actions.SPECIAL_ASSET_REQUEST_NEW, data: { requestType, requestId, name, channelName: name, channelId: name }, }; } export function onNewAssetRequest(name, id, channelName, channelId, extension) { const requestType = extension ? ASSET_LITE : ASSET_DETAILS; const requestId = `ar#${name}#${id}#${channelName}#${channelId}`; return { type: actions.ASSET_REQUEST_NEW, data: { requestType, requestId, name, modifier: { id, channel: { name: channelName, id: channelId, }, }, }, }; } export function onRequestUpdate(requestType, requestId) { return { type: actions.REQUEST_UPDATE, data: { requestType, requestId, }, }; } export function addRequestToRequestList(id, error, key) { return { type: actions.REQUEST_LIST_ADD, data: { id, error, key }, }; } // asset actions export function addAssetToAssetList(id, error, name, claimId, shortId, claimData, claimViews) { return { type: actions.ASSET_ADD, data: { id, error, name, claimId, shortId, claimData, claimViews }, }; } export function updateAssetViewsInList(id, claimId, claimViews) { return { type: actions.ASSET_VIEWS_UPDATE, data: { id, claimId, claimViews }, }; } export function removeAsset(data) { return { type: actions.ASSET_REMOVE, data, }; } // channel actions export function addNewChannelToChannelList(id, name, shortId, longId, claimsData) { return { type: actions.CHANNEL_ADD, data: { id, name, shortId, longId, claimsData, }, }; } export function onUpdateChannelClaims(channelKey, name, longId, page) { return { type: actions.CHANNEL_CLAIMS_UPDATE_ASYNC, data: { channelKey, name, longId, page }, }; } export function updateChannelClaims(channelListId, claimsData) { return { type: actions.CHANNEL_CLAIMS_UPDATE_SUCCEEDED, data: { channelListId, claimsData }, }; } // display a file export function fileRequested(name, claimId) { return { type: actions.FILE_REQUESTED, data: { name, claimId }, }; } export function updateFileAvailability(status) { return { type: actions.FILE_AVAILABILITY_UPDATE, data: status, }; } export function updateDisplayAssetError(error) { return { type: actions.DISPLAY_ASSET_ERROR, data: error, }; } // viewer settings export function toggleDetailsExpanded(isExpanded) { return { type: actions.TOGGLE_DETAILS_EXPANDED, data: isExpanded, }; } ================================================ FILE: client/src/api/assetApi.js ================================================ import Request from '../utils/request'; export function getLongClaimId(host, name, modifier) { let body = {}; // create request params if (modifier) { if (modifier.id) { body['claimId'] = modifier.id; } else { body['channelName'] = modifier.channel.name; body['channelClaimId'] = modifier.channel.id; } } body['claimName'] = name; const params = { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }; // create url const url = `${host}/api/claim/long-id`; // return the request promise return Request(url, params); } export function getShortId(host, name, claimId) { const url = `${host}/api/claim/short-id/${claimId}/${name}`; return Request(url); } export function getClaimData(host, name, claimId) { const url = `${host}/api/claim/data/${name}/${claimId}`; return Request(url); } export function checkClaimAvailability(claim) { const url = `/api/claim/availability/${claim}`; return Request(url); } export function getClaimViews(claimId) { const url = `/api/claim/views/${claimId}`; return Request(url); } export function doAbandonClaim(outpoint) { const params = { method: 'POST', body: JSON.stringify({ outpoint }), headers: new Headers({ 'Content-Type': 'application/json', }), credentials: 'include', }; return Request('/api/claim/abandon', params); } ================================================ FILE: client/src/api/authApi.js ================================================ import Request from '../utils/request'; export function checkForLoggedInChannelApi () { const url = `/user`; const params = {credentials: 'include'}; return Request(url, params); } export function channelLogoutApi () { const url = `/logout`; const params = {credentials: 'include'}; return Request(url, params); } ================================================ FILE: client/src/api/channelApi.js ================================================ import Request from '../utils/request'; export function getChannelData (host, name, id) { if (!id) id = 'none'; const url = `${host}/api/channel/data/${name}/${id}`; return Request(url); } export function getChannelClaims (host, name, longId, page) { if (!page) page = 1; const url = `${host}/api/channel/claims/${name}/${longId}/${page}`; return Request(url); } export function checkChannelAvailability (channel) { const url = `/api/channel/availability/${channel}`; return Request(url); } export function makeCreateChannelRequest (username, password) { const params = { method : 'POST', body : JSON.stringify({username, password}), headers: new Headers({ 'Content-Type': 'application/json', }), credentials: 'include', }; return Request('/signup', params); } ================================================ FILE: client/src/api/fileApi.js ================================================ import Request from '../utils/request'; export function checkFileAvailability (claimId, host, name) { const url = `${host}/api/file/availability/${name}/${claimId}`; return Request(url); } export function triggerClaimGet (claimId, host, name) { const url = `${host}/api/claim/get/${name}/${claimId}`; return Request(url); } ================================================ FILE: client/src/api/homepageApi.js ================================================ import Request from '../utils/request'; export function getHomepageChannelsData (host, name, id) { const url = `${host}/api/homepage/data/channels`; return Request(url); } ================================================ FILE: client/src/api/specialAssetApi.js ================================================ import Request from '../utils/request'; export function getSpecialAssetClaims (host, name, page) { if (!page) page = 1; const url = `${host}/api/special/${name}/${page}`; return Request(url); } ================================================ FILE: client/src/app.js ================================================ import React from 'react'; import { hot } from 'react-hot-loader/root' import { Route, Switch } from 'react-router-dom'; import HomePage from '@pages/HomePage'; import AboutPage from '@pages/AboutPage'; import TosPage from '@pages/TosPage'; import FaqPage from '@pages/FaqPage'; import LoginPage from '@pages/LoginPage'; import ContentPageWrapper from '@pages/ContentPageWrapper'; import FourOhFourPage from '@pages/FourOhFourPage'; import MultisitePage from '@pages/MultisitePage'; import PopularPage from '@pages/PopularPage'; import EditPage from '@pages/EditPage'; const App = () => { return ( ); }; export default hot(App); ================================================ FILE: client/src/channels/publish.js ================================================ import {buffers, END, eventChannel} from 'redux-saga'; export const makePublishRequestChannel = (fd, isUpdate) => { return eventChannel(emitter => { const uri = `/api/claim/${isUpdate ? 'update' : 'publish'}`; const xhr = new XMLHttpRequest(); // add event listeners const onLoadStart = () => { emitter({loadStart: true}); }; const onProgress = (event) => { if (event.lengthComputable) { const percentage = Math.round((event.loaded * 100) / event.total); emitter({progress: percentage}); } }; const onLoad = () => { emitter({load: true}); }; xhr.upload.addEventListener('loadstart', onLoadStart); xhr.upload.addEventListener('progress', onProgress); xhr.upload.addEventListener('load', onLoad); // set state change handler xhr.onreadystatechange = () => { if (xhr.readyState === XMLHttpRequest.DONE) { switch (xhr.status) { case 413: emitter({error: new Error("Unfortunately it appears this web server " + "has been misconfigured, please inform the service administrators " + "that they must set their nginx/apache request size maximums higher " + "than their file size limits.")}); emitter(END); break; case 200: var response = JSON.parse(xhr.response); if (response.success) { emitter({success: response}); emitter(END); } else { emitter({error: new Error(response.message)}); emitter(END); } break; default: emitter({error: new Error("Received an unexpected response from " + "server: " + xhr.status)}); emitter(END); } } }; // open and send xhr.open('POST', uri, true); xhr.send(fd); // clean up return () => { xhr.upload.removeEventListener('loadstart', onLoadStart); xhr.upload.removeEventListener('progress', onProgress); xhr.upload.removeEventListener('load', onLoad); xhr.onreadystatechange = null; xhr.abort(); }; }, buffers.sliding(2)); }; ================================================ FILE: client/src/components/AboutSpeechDetails/index.jsx ================================================ import React from 'react'; import Row from '@components/Row'; const AboutSpeechDetails = () => { return (

Spee.ch's journey may be on hold, but LBRY is still on mission. We'd like to thank all of our testers and early adopters for helping us explore this use case. We're really excited about lbry.tv and can't wait to see you over there for a fully featured experience.

); }; export default AboutSpeechDetails; ================================================ FILE: client/src/components/AboutSpeechOverview/index.jsx ================================================ import React from 'react'; import Row from '@components/Row'; const AboutSpeechOverview = () => { return (

Lbry is no longer supporting Spee.ch. However, we're excited to show you lbry.tv!

); }; export default AboutSpeechOverview; ================================================ FILE: client/src/components/ActiveStatusBar/index.jsx ================================================ import React from 'react'; const ActiveStatusBar = () => { return | ; }; export default ActiveStatusBar; ================================================ FILE: client/src/components/AssetInfoFooter/index.js ================================================ import React from 'react'; import Row from '@components/Row'; const AssetInfoFooter = ({ assetUrl, name }) => { return (

Hosted via the{' '} LBRY {' '} blockchain

); }; export default AssetInfoFooter; ================================================ FILE: client/src/components/AssetPreview/index.jsx ================================================ import React from 'react'; import { Link } from 'react-router-dom'; import createCanonicalLink from '@globalutils/createCanonicalLink'; import * as Icon from 'react-feather'; import Img from 'react-image'; const AssetPreview = ({ defaultThumbnail, claimData }) => { const {name, fileExt, contentType, thumbnail, title, blocked, transactionTime = 0} = claimData; const showUrl = createCanonicalLink({asset: {...claimData}}); const embedUrl = `${showUrl}.${fileExt}`; const ago = Date.now() / 1000 - transactionTime; const dayInSeconds = 60 * 60 * 24; const monthInSeconds = dayInSeconds * 30; let when; if (ago < dayInSeconds || transactionTime < 1) { when = 'Just today'; } else if (ago < monthInSeconds) { when = `${Math.floor(ago / dayInSeconds)} d ago`; } else { when = `${Math.floor(ago / monthInSeconds)} mo ago`; } /* we'll be assigning media icon based on supported type / mime types */ const media = contentType.split('/')[0]; /* make sure thumb has the right url */ const thumb = media === 'image' ? embedUrl : thumbnail; /* This blocked section shouldn't be necessary after pagination is reworked, though it might be useful for channel_mine situations. */ if (blocked) { return (

Error 451

This content is blocked for legal reasons.

Blocked Content

); } else { return (
{name}

{title}

{ media === 'image' && } { media === 'text' && } { media === 'video' && contentType === 'video/mp4' && } { media !== 'image' && media !== 'text' && contentType !== 'video/mp4' && }
{fileExt}
{when}
); } }; export default AssetPreview; ================================================ FILE: client/src/components/AssetShareButtons/index.js ================================================ import React from 'react'; import SocialShareLink from '@components/SocialShareLink'; const AssetShareButtons = ({ assetUrl, name }) => { return ( ); }; // // Additional icons disabled. If you want to add additional icons, you have to solve // https://github.com/lbryio/spee.ch/issues/687 // // // mastodon // // // diaspora // export default AssetShareButtons; ================================================ FILE: client/src/components/ButtonPrimary/index.jsx ================================================ import React from 'react'; const ButtonPrimary = ({ value, onClickHandler, type = 'button' }) => { return ( ); }; export default ButtonPrimary; ================================================ FILE: client/src/components/ButtonPrimaryJumbo/index.jsx ================================================ import React from 'react'; const ButtonPrimaryJumbo = ({ value, onClickHandler }) => { return ( ); }; export default ButtonPrimaryJumbo; ================================================ FILE: client/src/components/ButtonSecondary/index.jsx ================================================ import React from 'react'; const ButtonPrimary = ({ value, onClickHandler }) => { return ( ); }; export default ButtonPrimary; ================================================ FILE: client/src/components/ChannelAbout/index.jsx ================================================ import React from 'react'; const ChannelAbout = () => { return (

Channels allow you to publish and group content under an identity. You can create a channel for yourself, or share one with like-minded friends.

You can create 1 channel, or 100, so whether you're documenting important events, or making a public repository for cat gifs (password: '1234'), try creating a channel for it!

); }; export default ChannelAbout; ================================================ FILE: client/src/components/ChannelCreateNameInput/index.jsx ================================================ import React from 'react'; import Label from '@components/Label'; import RowLabeled from '@components/RowLabeled'; const ChannelCreateNameInput = ({ value, error, handleNameInput }) => { return ( } content={
@ { (value && !error) && ( {'\u2713'} )} { error && ( {'\u2716'} )}
} /> ); }; export default ChannelCreateNameInput; ================================================ FILE: client/src/components/ChannelCreatePasswordInput/index.jsx ================================================ import React from 'react'; import Label from '@components/Label'; import RowLabeled from '@components/RowLabeled'; const ChannelCreatePasswordInput = ({ value, handlePasswordInput }) => { return ( } content={
} /> ); }; export default ChannelCreatePasswordInput; ================================================ FILE: client/src/components/ChannelInfoDisplay/index.jsx ================================================ import React from 'react'; // TODO: factor out longId OR implement tooltip display const ChannelInfoDisplay = ({name, longId, shortId}) => { return (

{name}:{shortId}

); }; export default ChannelInfoDisplay; ================================================ FILE: client/src/components/ChannelLoginNameInput/index.jsx ================================================ import React from 'react'; import RowLabeled from '@components/RowLabeled'; import Label from '@components/Label'; const ChannelLoginNameInput = ({ channelName, handleInput }) => { return ( } content={
@
} /> ); }; export default ChannelLoginNameInput; ================================================ FILE: client/src/components/ChannelLoginPasswordInput/index.jsx ================================================ import React from 'react'; import RowLabeled from '@components/RowLabeled'; import Label from '@components/Label'; const ChannelLoginPasswordInput = ({ channelPassword, handleInput }) => { return ( } content={
} /> ); }; export default ChannelLoginPasswordInput; ================================================ FILE: client/src/components/ChannelSelectDropdown/index.jsx ================================================ import React from 'react'; import { LOGIN, CREATE } from '../../constants/publish_channel_select_states'; const ChannelSelectDropdown = ({ selectedChannel, handleSelection, loggedInChannelName }) => { return ( ); }; export default ChannelSelectDropdown; ================================================ FILE: client/src/components/ChooseAnonymousPublishRadio/index.jsx ================================================ import React from 'react'; const ChooseAnonymousPublishRadio = ({ publishInChannel, toggleAnonymousPublish }) => { return (
); }; export default ChooseAnonymousPublishRadio; ================================================ FILE: client/src/components/ChooseChannelPublishRadio/index.jsx ================================================ import React from 'react'; const ChooseChannelPublishRadio = ({ publishInChannel, toggleAnonymousPublish }) => { return (
); }; export default ChooseChannelPublishRadio; ================================================ FILE: client/src/components/ClickToCopy/index.jsx ================================================ import React from 'react'; import * as Icon from 'react-feather'; class ClickToCopy extends React.Component { constructor (props) { super(props); this.copyToClipboard = this.copyToClipboard.bind(this); } copyToClipboard () { const elementToCopy = this.props.id; const element = document.getElementById(elementToCopy); console.log(elementToCopy); element.select(); try { document.execCommand('copy'); } catch (err) { this.setState({error: 'Oops, unable to copy'}); } } render () { const {id, value} = this.props; return (
); } } export default ClickToCopy; ================================================ FILE: client/src/components/DropzoneDropItDisplay/index.jsx ================================================ import React from 'react'; const DropzoneDropItDisplay = () => { return (
Drop it.
); }; export default DropzoneDropItDisplay; ================================================ FILE: client/src/components/DropzoneInstructionsDisplay/index.jsx ================================================ import React from 'react'; import FormFeedbackDisplay from '@components/FormFeedbackDisplay'; import Row from '@components/Row'; const DropzoneInstructionsDisplay = ({fileError, message}) => { if (!message) { message = 'Drag & drop image or video here to publish'; } return (
{message} OR { fileError ? (
CHOOSE FILE
) : ( CHOOSE FILE )}
); }; export default DropzoneInstructionsDisplay; ================================================ FILE: client/src/components/DropzonePreviewImage/index.jsx ================================================ import React from 'react'; import PropTypes from 'prop-types'; class PublishPreview extends React.Component { constructor (props) { super(props); this.state = { imgSource : '', defaultVideoThumbnail: '/assets/img/video_thumb_default.png', defaultThumbnail : '/assets/img/Speech_Logo_Main@OG-02.jpg', }; } componentDidMount () { const { isUpdate, sourceUrl, file } = this.props; if (isUpdate && sourceUrl) { this.setState({ imgSource: sourceUrl }); } else { this.setPreviewImageSource(file); } } componentWillReceiveProps (newProps) { if (newProps.file !== this.props.file) { this.setPreviewImageSource(newProps.file); } if (newProps.thumbnail !== this.props.thumbnail) { if (newProps.thumbnail) { this.setPreviewImageSourceFromFile(newProps.thumbnail); } else { this.setState({imgSource: this.state.defaultThumbnail}); } } } setPreviewImageSourceFromFile (file) { const previewReader = new FileReader(); previewReader.readAsDataURL(file); previewReader.onloadend = () => { this.setState({imgSource: previewReader.result}); }; } setPreviewImageSource (file) { if (this.props.thumbnail) { this.setPreviewImageSourceFromFile(this.props.thumbnail); } else if (file.type.substr(0, file.type.indexOf('/')) === 'image'){ this.setPreviewImageSourceFromFile(file); } else if (file.type === 'video'){ this.setState({imgSource: this.state.defaultVideoThumbnail}); } else { this.setState({imgSource: this.state.defaultThumbnail}); } } render () { return ( publish preview ); } }; PublishPreview.propTypes = { dimPreview: PropTypes.bool.isRequired, file : PropTypes.object, thumbnail : PropTypes.object, isUpdate : PropTypes.bool, sourceUrl : PropTypes.string, }; export default PublishPreview; ================================================ FILE: client/src/components/ErrorBoundary/index.jsx ================================================ import React from 'react'; class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; } componentDidCatch(error, info) { // Display fallback UI this.setState({ hasError: true }); // You can also log the error to an error reporting service console.log('Error occurred while rendering markdown') } render() { if (this.state.hasError) { // You can render any custom fallback UI return (

A component was prevented from crashing the App.

); } return this.props.children; } } export default ErrorBoundary; ================================================ FILE: client/src/components/ExpandingTextArea/index.jsx ================================================ import React, { Component } from 'react'; import PropTypes from 'prop-types'; class ExpandingTextarea extends Component { constructor (props) { super(props); this._handleChange = this._handleChange.bind(this); } componentDidMount () { this.adjustTextarea({}); } _handleChange (event) { const { onChange } = this.props; if (onChange) onChange(event); this.adjustTextarea(event); } adjustTextarea ({ target = this.el }) { target.style.height = 0; target.style.height = `${target.scrollHeight}px`; } render () { const { ...rest } = this.props; return (