Эх сурвалжийг харах

Migrate frontend to react

niels 4 жил өмнө
parent
commit
6dfe90dccc
85 өөрчлөгдсөн 4185 нэмэгдсэн , 290 устгасан
  1. 1 1
      chube.py
  2. 101 0
      frontend/chuchufe/config/env.js
  3. 66 0
      frontend/chuchufe/config/getHttpsConfig.js
  4. 14 0
      frontend/chuchufe/config/jest/cssTransform.js
  5. 40 0
      frontend/chuchufe/config/jest/fileTransform.js
  6. 141 0
      frontend/chuchufe/config/modules.js
  7. 72 0
      frontend/chuchufe/config/paths.js
  8. 35 0
      frontend/chuchufe/config/pnpTs.js
  9. 669 0
      frontend/chuchufe/config/webpack.config.js
  10. 130 0
      frontend/chuchufe/config/webpackDevServer.config.js
  11. 110 5
      frontend/chuchufe/package.json
  12. 38 40
      frontend/chuchufe/public/index.html
  13. 0 43
      frontend/chuchufe/public/player.html
  14. 211 0
      frontend/chuchufe/scripts/build.js
  15. 166 0
      frontend/chuchufe/scripts/start.js
  16. 53 0
      frontend/chuchufe/scripts/test.js
  17. 24 8
      frontend/chuchufe/src/App.js
  18. 29 0
      frontend/chuchufe/src/components/ChooseRoom.js
  19. 8 0
      frontend/chuchufe/src/components/NavBar.js
  20. 82 0
      frontend/chuchufe/src/components/Player.js
  21. 90 0
      frontend/chuchufe/src/components/PlayerToolbar.js
  22. 56 0
      frontend/chuchufe/src/components/Queue.js
  23. 34 0
      frontend/chuchufe/src/components/Room.js
  24. 34 0
      frontend/chuchufe/src/components/Search.js
  25. 68 0
      frontend/chuchufe/src/components/SearchResults.js
  26. 137 0
      frontend/chuchufe/src/components/YoutubePlayer.js
  27. 0 0
      frontend/chuchufe/src/css/bootstrap/bootstrap.css
  28. 0 0
      frontend/chuchufe/src/css/bootstrap/bootstrap.css.map
  29. 0 0
      frontend/chuchufe/src/css/bootstrap/bootstrap.min.css
  30. 0 0
      frontend/chuchufe/src/css/bootstrap/bootstrap.min.css.map
  31. 0 0
      frontend/chuchufe/src/css/fontawesome.css
  32. 0 0
      frontend/chuchufe/src/css/fontawesome.min.css
  33. 27 7
      frontend/chuchufe/src/css/main.css
  34. 0 0
      frontend/chuchufe/src/css/normalize.css
  35. 0 0
      frontend/chuchufe/src/css/player.css
  36. 0 0
      frontend/chuchufe/src/css/regular.css
  37. 0 0
      frontend/chuchufe/src/css/regular.min.css
  38. 0 0
      frontend/chuchufe/src/css/solid.css
  39. 0 0
      frontend/chuchufe/src/css/solid.min.css
  40. BIN
      frontend/chuchufe/src/img/favicon.png
  41. BIN
      frontend/chuchufe/src/img/icon.png
  42. 633 0
      frontend/chuchufe/src/img/icon.svg
  43. BIN
      frontend/chuchufe/src/img/logo-100.png
  44. BIN
      frontend/chuchufe/src/img/logo.png
  45. 614 0
      frontend/chuchufe/src/img/logo.svg
  46. BIN
      frontend/chuchufe/src/img/no_thumbnail.png
  47. 105 0
      frontend/chuchufe/src/img/no_thumbnail.svg
  48. 1 1
      frontend/chuchufe/src/index.js
  49. 0 87
      frontend/chuchufe/src/useRoom.js
  50. 1 1
      frontend/chuchufe/src/util/Resolver.js
  51. 9 0
      frontend/chuchufe/src/util/enums.js
  52. 11 0
      frontend/chuchufe/src/util/processors/controlProcessor.js
  53. 66 0
      frontend/chuchufe/src/util/processors/listOperationProcessor.js
  54. 38 0
      frontend/chuchufe/src/util/processors/mediaActionProcessor.js
  55. 7 0
      frontend/chuchufe/src/util/processors/searchIdResultProcessor.js
  56. 38 0
      frontend/chuchufe/src/util/processors/songEndProcessor.js
  57. 52 0
      frontend/chuchufe/src/util/processors/stateProcessor.js
  58. 34 8
      frontend/chuchufe/src/util/room.js
  59. 0 0
      frontend/chuchufe/src/util/serviceWorker.js
  60. 0 0
      frontend/chuchufe/src/util/setupTests.js
  61. 27 0
      frontend/chuchufe/src/util/useMapBuilder.js
  62. 84 0
      frontend/chuchufe/src/util/useRoom.js
  63. 0 0
      frontend/chuchufe/src/webfonts/fa-brands-400.eot
  64. 0 0
      frontend/chuchufe/src/webfonts/fa-brands-400.svg
  65. 0 0
      frontend/chuchufe/src/webfonts/fa-brands-400.ttf
  66. 0 0
      frontend/chuchufe/src/webfonts/fa-brands-400.woff
  67. 0 0
      frontend/chuchufe/src/webfonts/fa-brands-400.woff2
  68. 0 0
      frontend/chuchufe/src/webfonts/fa-regular-400.eot
  69. 0 0
      frontend/chuchufe/src/webfonts/fa-regular-400.svg
  70. 0 0
      frontend/chuchufe/src/webfonts/fa-regular-400.ttf
  71. 0 0
      frontend/chuchufe/src/webfonts/fa-regular-400.woff
  72. 0 0
      frontend/chuchufe/src/webfonts/fa-regular-400.woff2
  73. 0 0
      frontend/chuchufe/src/webfonts/fa-solid-900.eot
  74. 0 0
      frontend/chuchufe/src/webfonts/fa-solid-900.svg
  75. 0 0
      frontend/chuchufe/src/webfonts/fa-solid-900.ttf
  76. 0 0
      frontend/chuchufe/src/webfonts/fa-solid-900.woff
  77. 0 0
      frontend/chuchufe/src/webfonts/fa-solid-900.woff2
  78. 0 0
      frontend/chuchufe/src/webfonts/kenyc.ttf
  79. 5 65
      frontend/chuchufe/yarn.lock
  80. 1 1
      static/css/main.css
  81. 2 2
      static/css/regular.css
  82. 1 1
      static/css/regular.min.css
  83. 2 2
      static/css/solid.css
  84. 1 1
      static/css/solid.min.css
  85. 17 17
      static/js/main.js

+ 1 - 1
chube.py

@@ -271,7 +271,6 @@ async def release_control_processor(ws, data, path):
         await release_control(ws, room)
     else:
         pass
-        # TODO error here
 
 
 def play_next_song(room):
@@ -308,6 +307,7 @@ async def player_enabled_processor(ws, data, path):
         await release_control(ws, room)
 
 
+# TODO change OBTAIN_CONTROL en RELEASE_CONTROL to one message
 # TODO There is some potential concurrent bug here, when the controller loses/releases control right before a song end.
 async def obtain_control(ws, room: Room):
     with room.controller_lock:

+ 101 - 0
frontend/chuchufe/config/env.js

@@ -0,0 +1,101 @@
+'use strict';
+
+const fs = require('fs');
+const path = require('path');
+const paths = require('./paths');
+
+// Make sure that including paths.js after env.js will read .env variables.
+delete require.cache[require.resolve('./paths')];
+
+const NODE_ENV = process.env.NODE_ENV;
+if (!NODE_ENV) {
+  throw new Error(
+    'The NODE_ENV environment variable is required but was not specified.'
+  );
+}
+
+// https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use
+const dotenvFiles = [
+  `${paths.dotenv}.${NODE_ENV}.local`,
+  `${paths.dotenv}.${NODE_ENV}`,
+  // Don't include `.env.local` for `test` environment
+  // since normally you expect tests to produce the same
+  // results for everyone
+  NODE_ENV !== 'test' && `${paths.dotenv}.local`,
+  paths.dotenv,
+].filter(Boolean);
+
+// Load environment variables from .env* files. Suppress warnings using silent
+// if this file is missing. dotenv will never modify any environment variables
+// that have already been set.  Variable expansion is supported in .env files.
+// https://github.com/motdotla/dotenv
+// https://github.com/motdotla/dotenv-expand
+dotenvFiles.forEach(dotenvFile => {
+  if (fs.existsSync(dotenvFile)) {
+    require('dotenv-expand')(
+      require('dotenv').config({
+        path: dotenvFile,
+      })
+    );
+  }
+});
+
+// We support resolving modules according to `NODE_PATH`.
+// This lets you use absolute paths in imports inside large monorepos:
+// https://github.com/facebook/create-react-app/issues/253.
+// It works similar to `NODE_PATH` in Node itself:
+// https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders
+// Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored.
+// Otherwise, we risk importing Node.js core modules into an app instead of webpack shims.
+// https://github.com/facebook/create-react-app/issues/1023#issuecomment-265344421
+// We also resolve them to make sure all tools using them work consistently.
+const appDirectory = fs.realpathSync(process.cwd());
+process.env.NODE_PATH = (process.env.NODE_PATH || '')
+  .split(path.delimiter)
+  .filter(folder => folder && !path.isAbsolute(folder))
+  .map(folder => path.resolve(appDirectory, folder))
+  .join(path.delimiter);
+
+// Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be
+// injected into the application via DefinePlugin in webpack configuration.
+const REACT_APP = /^REACT_APP_/i;
+
+function getClientEnvironment(publicUrl) {
+  const raw = Object.keys(process.env)
+    .filter(key => REACT_APP.test(key))
+    .reduce(
+      (env, key) => {
+        env[key] = process.env[key];
+        return env;
+      },
+      {
+        // Useful for determining whether we’re running in production mode.
+        // Most importantly, it switches React into the correct mode.
+        NODE_ENV: process.env.NODE_ENV || 'development',
+        // Useful for resolving the correct path to static assets in `public`.
+        // For example, <img src={process.env.PUBLIC_URL + '/img/logo.png'} />.
+        // This should only be used as an escape hatch. Normally you would put
+        // images into the `src` and `import` them in code to get their paths.
+        PUBLIC_URL: publicUrl,
+        // We support configuring the sockjs pathname during development.
+        // These settings let a developer run multiple simultaneous projects.
+        // They are used as the connection `hostname`, `pathname` and `port`
+        // in webpackHotDevClient. They are used as the `sockHost`, `sockPath`
+        // and `sockPort` options in webpack-dev-server.
+        WDS_SOCKET_HOST: process.env.WDS_SOCKET_HOST,
+        WDS_SOCKET_PATH: process.env.WDS_SOCKET_PATH,
+        WDS_SOCKET_PORT: process.env.WDS_SOCKET_PORT,
+      }
+    );
+  // Stringify all values so we can feed into webpack DefinePlugin
+  const stringified = {
+    'process.env': Object.keys(raw).reduce((env, key) => {
+      env[key] = JSON.stringify(raw[key]);
+      return env;
+    }, {}),
+  };
+
+  return { raw, stringified };
+}
+
+module.exports = getClientEnvironment;

+ 66 - 0
frontend/chuchufe/config/getHttpsConfig.js

@@ -0,0 +1,66 @@
+'use strict';
+
+const fs = require('fs');
+const path = require('path');
+const crypto = require('crypto');
+const chalk = require('react-dev-utils/chalk');
+const paths = require('./paths');
+
+// Ensure the certificate and key provided are valid and if not
+// throw an easy to debug error
+function validateKeyAndCerts({ cert, key, keyFile, crtFile }) {
+  let encrypted;
+  try {
+    // publicEncrypt will throw an error with an invalid cert
+    encrypted = crypto.publicEncrypt(cert, Buffer.from('test'));
+  } catch (err) {
+    throw new Error(
+      `The certificate "${chalk.yellow(crtFile)}" is invalid.\n${err.message}`
+    );
+  }
+
+  try {
+    // privateDecrypt will throw an error with an invalid key
+    crypto.privateDecrypt(key, encrypted);
+  } catch (err) {
+    throw new Error(
+      `The certificate key "${chalk.yellow(keyFile)}" is invalid.\n${
+        err.message
+      }`
+    );
+  }
+}
+
+// Read file and throw an error if it doesn't exist
+function readEnvFile(file, type) {
+  if (!fs.existsSync(file)) {
+    throw new Error(
+      `You specified ${chalk.cyan(
+        type
+      )} in your env, but the file "${chalk.yellow(file)}" can't be found.`
+    );
+  }
+  return fs.readFileSync(file);
+}
+
+// Get the https config
+// Return cert files if provided in env, otherwise just true or false
+function getHttpsConfig() {
+  const { SSL_CRT_FILE, SSL_KEY_FILE, HTTPS } = process.env;
+  const isHttps = HTTPS === 'true';
+
+  if (isHttps && SSL_CRT_FILE && SSL_KEY_FILE) {
+    const crtFile = path.resolve(paths.appPath, SSL_CRT_FILE);
+    const keyFile = path.resolve(paths.appPath, SSL_KEY_FILE);
+    const config = {
+      cert: readEnvFile(crtFile, 'SSL_CRT_FILE'),
+      key: readEnvFile(keyFile, 'SSL_KEY_FILE'),
+    };
+
+    validateKeyAndCerts({ ...config, keyFile, crtFile });
+    return config;
+  }
+  return isHttps;
+}
+
+module.exports = getHttpsConfig;

+ 14 - 0
frontend/chuchufe/config/jest/cssTransform.js

@@ -0,0 +1,14 @@
+'use strict';
+
+// This is a custom Jest transformer turning style imports into empty objects.
+// http://facebook.github.io/jest/docs/en/webpack.html
+
+module.exports = {
+  process() {
+    return 'module.exports = {};';
+  },
+  getCacheKey() {
+    // The output is always the same.
+    return 'cssTransform';
+  },
+};

+ 40 - 0
frontend/chuchufe/config/jest/fileTransform.js

@@ -0,0 +1,40 @@
+'use strict';
+
+const path = require('path');
+const camelcase = require('camelcase');
+
+// This is a custom Jest transformer turning file imports into filenames.
+// http://facebook.github.io/jest/docs/en/webpack.html
+
+module.exports = {
+  process(src, filename) {
+    const assetFilename = JSON.stringify(path.basename(filename));
+
+    if (filename.match(/\.svg$/)) {
+      // Based on how SVGR generates a component name:
+      // https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6
+      const pascalCaseFilename = camelcase(path.parse(filename).name, {
+        pascalCase: true,
+      });
+      const componentName = `Svg${pascalCaseFilename}`;
+      return `const React = require('react');
+      module.exports = {
+        __esModule: true,
+        default: ${assetFilename},
+        ReactComponent: React.forwardRef(function ${componentName}(props, ref) {
+          return {
+            $$typeof: Symbol.for('react.element'),
+            type: 'svg',
+            ref: ref,
+            key: null,
+            props: Object.assign({}, props, {
+              children: ${assetFilename}
+            })
+          };
+        }),
+      };`;
+    }
+
+    return `module.exports = ${assetFilename};`;
+  },
+};

+ 141 - 0
frontend/chuchufe/config/modules.js

@@ -0,0 +1,141 @@
+'use strict';
+
+const fs = require('fs');
+const path = require('path');
+const paths = require('./paths');
+const chalk = require('react-dev-utils/chalk');
+const resolve = require('resolve');
+
+/**
+ * Get additional module paths based on the baseUrl of a compilerOptions object.
+ *
+ * @param {Object} options
+ */
+function getAdditionalModulePaths(options = {}) {
+  const baseUrl = options.baseUrl;
+
+  // We need to explicitly check for null and undefined (and not a falsy value) because
+  // TypeScript treats an empty string as `.`.
+  if (baseUrl == null) {
+    // If there's no baseUrl set we respect NODE_PATH
+    // Note that NODE_PATH is deprecated and will be removed
+    // in the next major release of create-react-app.
+
+    const nodePath = process.env.NODE_PATH || '';
+    return nodePath.split(path.delimiter).filter(Boolean);
+  }
+
+  const baseUrlResolved = path.resolve(paths.appPath, baseUrl);
+
+  // We don't need to do anything if `baseUrl` is set to `node_modules`. This is
+  // the default behavior.
+  if (path.relative(paths.appNodeModules, baseUrlResolved) === '') {
+    return null;
+  }
+
+  // Allow the user set the `baseUrl` to `appSrc`.
+  if (path.relative(paths.appSrc, baseUrlResolved) === '') {
+    return [paths.appSrc];
+  }
+
+  // If the path is equal to the root directory we ignore it here.
+  // We don't want to allow importing from the root directly as source files are
+  // not transpiled outside of `src`. We do allow importing them with the
+  // absolute path (e.g. `src/Components/Button.js`) but we set that up with
+  // an alias.
+  if (path.relative(paths.appPath, baseUrlResolved) === '') {
+    return null;
+  }
+
+  // Otherwise, throw an error.
+  throw new Error(
+    chalk.red.bold(
+      "Your project's `baseUrl` can only be set to `src` or `node_modules`." +
+        ' Create React App does not support other values at this time.'
+    )
+  );
+}
+
+/**
+ * Get webpack aliases based on the baseUrl of a compilerOptions object.
+ *
+ * @param {*} options
+ */
+function getWebpackAliases(options = {}) {
+  const baseUrl = options.baseUrl;
+
+  if (!baseUrl) {
+    return {};
+  }
+
+  const baseUrlResolved = path.resolve(paths.appPath, baseUrl);
+
+  if (path.relative(paths.appPath, baseUrlResolved) === '') {
+    return {
+      src: paths.appSrc,
+    };
+  }
+}
+
+/**
+ * Get jest aliases based on the baseUrl of a compilerOptions object.
+ *
+ * @param {*} options
+ */
+function getJestAliases(options = {}) {
+  const baseUrl = options.baseUrl;
+
+  if (!baseUrl) {
+    return {};
+  }
+
+  const baseUrlResolved = path.resolve(paths.appPath, baseUrl);
+
+  if (path.relative(paths.appPath, baseUrlResolved) === '') {
+    return {
+      '^src/(.*)$': '<rootDir>/src/$1',
+    };
+  }
+}
+
+function getModules() {
+  // Check if TypeScript is setup
+  const hasTsConfig = fs.existsSync(paths.appTsConfig);
+  const hasJsConfig = fs.existsSync(paths.appJsConfig);
+
+  if (hasTsConfig && hasJsConfig) {
+    throw new Error(
+      'You have both a tsconfig.json and a jsconfig.json. If you are using TypeScript please remove your jsconfig.json file.'
+    );
+  }
+
+  let config;
+
+  // If there's a tsconfig.json we assume it's a
+  // TypeScript project and set up the config
+  // based on tsconfig.json
+  if (hasTsConfig) {
+    const ts = require(resolve.sync('typescript', {
+      basedir: paths.appNodeModules,
+    }));
+    config = ts.readConfigFile(paths.appTsConfig, ts.sys.readFile).config;
+    // Otherwise we'll check if there is jsconfig.json
+    // for non TS projects.
+  } else if (hasJsConfig) {
+    config = require(paths.appJsConfig);
+  }
+
+  config = config || {};
+  const options = config.compilerOptions || {};
+
+  const additionalModulePaths = getAdditionalModulePaths(options);
+
+  return {
+    additionalModulePaths: additionalModulePaths,
+    webpackAliases: getWebpackAliases(options),
+    jestAliases: getJestAliases(options),
+    hasTsConfig,
+  };
+}
+
+module.exports = getModules();

+ 72 - 0
frontend/chuchufe/config/paths.js

@@ -0,0 +1,72 @@
+'use strict';
+
+const path = require('path');
+const fs = require('fs');
+const getPublicUrlOrPath = require('react-dev-utils/getPublicUrlOrPath');
+
+// Make sure any symlinks in the project folder are resolved:
+// https://github.com/facebook/create-react-app/issues/637
+const appDirectory = fs.realpathSync(process.cwd());
+const resolveApp = relativePath => path.resolve(appDirectory, relativePath);
+
+// We use `PUBLIC_URL` environment variable or "homepage" field to infer
+// "public path" at which the app is served.
+// webpack needs to know it to put the right <script> hrefs into HTML even in
+// single-page apps that may serve index.html for nested URLs like /todos/42.
+// We can't use a relative path in HTML because we don't want to load something
+// like /todos/42/static/js/bundle.7289d.js. We have to know the root.
+const publicUrlOrPath = getPublicUrlOrPath(
+  process.env.NODE_ENV === 'development',
+  require(resolveApp('package.json')).homepage,
+  process.env.PUBLIC_URL
+);
+
+const moduleFileExtensions = [
+  'web.mjs',
+  'mjs',
+  'web.js',
+  'js',
+  'web.ts',
+  'ts',
+  'web.tsx',
+  'tsx',
+  'json',
+  'web.jsx',
+  'jsx',
+];
+
+// Resolve file paths in the same order as webpack
+const resolveModule = (resolveFn, filePath) => {
+  const extension = moduleFileExtensions.find(extension =>
+    fs.existsSync(resolveFn(`${filePath}.${extension}`))
+  );
+
+  if (extension) {
+    return resolveFn(`${filePath}.${extension}`);
+  }
+
+  return resolveFn(`${filePath}.js`);
+};
+
+// config after eject: we're in ./config/
+module.exports = {
+  dotenv: resolveApp('.env'),
+  appPath: resolveApp('.'),
+  appBuild: resolveApp('build'),
+  appPublic: resolveApp('public'),
+  appHtml: resolveApp('public/index.html'),
+  appIndexJs: resolveModule(resolveApp, 'src/index'),
+  appPackageJson: resolveApp('package.json'),
+  appSrc: resolveApp('src'),
+  appTsConfig: resolveApp('tsconfig.json'),
+  appJsConfig: resolveApp('jsconfig.json'),
+  yarnLockFile: resolveApp('yarn.lock'),
+  testsSetup: resolveModule(resolveApp, 'src/setupTests'),
+  proxySetup: resolveApp('src/setupProxy.js'),
+  appNodeModules: resolveApp('node_modules'),
+  publicUrlOrPath,
+};
+
+
+
+module.exports.moduleFileExtensions = moduleFileExtensions;

+ 35 - 0
frontend/chuchufe/config/pnpTs.js

@@ -0,0 +1,35 @@
+'use strict';
+
+const { resolveModuleName } = require('ts-pnp');
+
+exports.resolveModuleName = (
+  typescript,
+  moduleName,
+  containingFile,
+  compilerOptions,
+  resolutionHost
+) => {
+  return resolveModuleName(
+    moduleName,
+    containingFile,
+    compilerOptions,
+    resolutionHost,
+    typescript.resolveModuleName
+  );
+};
+
+exports.resolveTypeReferenceDirective = (
+  typescript,
+  moduleName,
+  containingFile,
+  compilerOptions,
+  resolutionHost
+) => {
+  return resolveModuleName(
+    moduleName,
+    containingFile,
+    compilerOptions,
+    resolutionHost,
+    typescript.resolveTypeReferenceDirective
+  );
+};

+ 669 - 0
frontend/chuchufe/config/webpack.config.js

@@ -0,0 +1,669 @@
+'use strict';
+
+const fs = require('fs');
+const path = require('path');
+const webpack = require('webpack');
+const resolve = require('resolve');
+const PnpWebpackPlugin = require('pnp-webpack-plugin');
+const HtmlWebpackPlugin = require('html-webpack-plugin');
+const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
+const InlineChunkHtmlPlugin = require('react-dev-utils/InlineChunkHtmlPlugin');
+const TerserPlugin = require('terser-webpack-plugin');
+const MiniCssExtractPlugin = require('mini-css-extract-plugin');
+const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
+const safePostCssParser = require('postcss-safe-parser');
+const ManifestPlugin = require('webpack-manifest-plugin');
+const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
+const WorkboxWebpackPlugin = require('workbox-webpack-plugin');
+const WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin');
+const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin');
+const getCSSModuleLocalIdent = require('react-dev-utils/getCSSModuleLocalIdent');
+const paths = require('./paths');
+const modules = require('./modules');
+const getClientEnvironment = require('./env');
+const ModuleNotFoundPlugin = require('react-dev-utils/ModuleNotFoundPlugin');
+const ForkTsCheckerWebpackPlugin = require('react-dev-utils/ForkTsCheckerWebpackPlugin');
+const typescriptFormatter = require('react-dev-utils/typescriptFormatter');
+
+const postcssNormalize = require('postcss-normalize');
+
+const appPackageJson = require(paths.appPackageJson);
+
+// Source maps are resource heavy and can cause out of memory issue for large source files.
+const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false';
+// Some apps do not need the benefits of saving a web request, so not inlining the chunk
+// makes for a smoother build process.
+const shouldInlineRuntimeChunk = process.env.INLINE_RUNTIME_CHUNK !== 'false';
+
+const isExtendingEslintConfig = process.env.EXTEND_ESLINT === 'true';
+
+const imageInlineSizeLimit = parseInt(
+  process.env.IMAGE_INLINE_SIZE_LIMIT || '10000'
+);
+
+// Check if TypeScript is setup
+const useTypeScript = fs.existsSync(paths.appTsConfig);
+
+// style files regexes
+const cssRegex = /\.css$/;
+const cssModuleRegex = /\.module\.css$/;
+const sassRegex = /\.(scss|sass)$/;
+const sassModuleRegex = /\.module\.(scss|sass)$/;
+
+// This is the production and development configuration.
+// It is focused on developer experience, fast rebuilds, and a minimal bundle.
+module.exports = function(webpackEnv) {
+  const isEnvDevelopment = webpackEnv === 'development';
+  const isEnvProduction = webpackEnv === 'production';
+
+  // Variable used for enabling profiling in Production
+  // passed into alias object. Uses a flag if passed into the build command
+  const isEnvProductionProfile =
+    isEnvProduction && process.argv.includes('--profile');
+
+  // We will provide `paths.publicUrlOrPath` to our app
+  // as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript.
+  // Omit trailing slash as %PUBLIC_URL%/xyz looks better than %PUBLIC_URL%xyz.
+  // Get environment variables to inject into our app.
+  const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1));
+
+  // common function to get style loaders
+  const getStyleLoaders = (cssOptions, preProcessor) => {
+    const loaders = [
+      isEnvDevelopment && require.resolve('style-loader'),
+      isEnvProduction && {
+        loader: MiniCssExtractPlugin.loader,
+        // css is located in `static/css`, use '../../' to locate index.html folder
+        // in production `paths.publicUrlOrPath` can be a relative path
+        options: paths.publicUrlOrPath.startsWith('.')
+          ? { publicPath: '../../' }
+          : {},
+      },
+      {
+        loader: require.resolve('css-loader'),
+        options: cssOptions,
+      },
+      {
+        // Options for PostCSS as we reference these options twice
+        // Adds vendor prefixing based on your specified browser support in
+        // package.json
+        loader: require.resolve('postcss-loader'),
+        options: {
+          // Necessary for external CSS imports to work
+          // https://github.com/facebook/create-react-app/issues/2677
+          ident: 'postcss',
+          plugins: () => [
+            require('postcss-flexbugs-fixes'),
+            require('postcss-preset-env')({
+              autoprefixer: {
+                flexbox: 'no-2009',
+              },
+              stage: 3,
+            }),
+            // Adds PostCSS Normalize as the reset css with default options,
+            // so that it honors browserslist config in package.json
+            // which in turn let's users customize the target behavior as per their needs.
+            postcssNormalize(),
+          ],
+          sourceMap: isEnvProduction && shouldUseSourceMap,
+        },
+      },
+    ].filter(Boolean);
+    if (preProcessor) {
+      loaders.push(
+        {
+          loader: require.resolve('resolve-url-loader'),
+          options: {
+            sourceMap: isEnvProduction && shouldUseSourceMap,
+          },
+        },
+        {
+          loader: require.resolve(preProcessor),
+          options: {
+            sourceMap: true,
+          },
+        }
+      );
+    }
+    return loaders;
+  };
+
+  return {
+    mode: isEnvProduction ? 'production' : isEnvDevelopment && 'development',
+    // Stop compilation early in production
+    bail: isEnvProduction,
+    devtool: isEnvProduction
+      ? shouldUseSourceMap
+        ? 'source-map'
+        : false
+      : isEnvDevelopment && 'cheap-module-source-map',
+    // These are the "entry points" to our application.
+    // This means they will be the "root" imports that are included in JS bundle.
+    entry: [
+      // Include an alternative client for WebpackDevServer. A client's job is to
+      // connect to WebpackDevServer by a socket and get notified about changes.
+      // When you save a file, the client will either apply hot updates (in case
+      // of CSS changes), or refresh the page (in case of JS changes). When you
+      // make a syntax error, this client will display a syntax error overlay.
+      // Note: instead of the default WebpackDevServer client, we use a custom one
+      // to bring better experience for Create React App users. You can replace
+      // the line below with these two lines if you prefer the stock client:
+      // require.resolve('webpack-dev-server/client') + '?/',
+      // require.resolve('webpack/hot/dev-server'),
+      isEnvDevelopment &&
+        require.resolve('react-dev-utils/webpackHotDevClient'),
+      // Finally, this is your app's code:
+      paths.appIndexJs,
+      // We include the app code last so that if there is a runtime error during
+      // initialization, it doesn't blow up the WebpackDevServer client, and
+      // changing JS code would still trigger a refresh.
+    ].filter(Boolean),
+    output: {
+      // The build folder.
+      path: isEnvProduction ? paths.appBuild : undefined,
+      // Add /* filename */ comments to generated require()s in the output.
+      pathinfo: isEnvDevelopment,
+      // There will be one main bundle, and one file per asynchronous chunk.
+      // In development, it does not produce real files.
+      filename: isEnvProduction
+        ? 'static/js/[name].[contenthash:8].js'
+        : isEnvDevelopment && 'static/js/bundle.js',
+      // TODO: remove this when upgrading to webpack 5
+      futureEmitAssets: true,
+      // There are also additional JS chunk files if you use code splitting.
+      chunkFilename: isEnvProduction
+        ? 'static/js/[name].[contenthash:8].chunk.js'
+        : isEnvDevelopment && 'static/js/[name].chunk.js',
+      // webpack uses `publicPath` to determine where the app is being served from.
+      // It requires a trailing slash, or the file assets will get an incorrect path.
+      // We inferred the "public path" (such as / or /my-project) from homepage.
+      publicPath: paths.publicUrlOrPath,
+      // Point sourcemap entries to original disk location (format as URL on Windows)
+      devtoolModuleFilenameTemplate: isEnvProduction
+        ? info =>
+            path
+              .relative(paths.appSrc, info.absoluteResourcePath)
+              .replace(/\\/g, '/')
+        : isEnvDevelopment &&
+          (info => path.resolve(info.absoluteResourcePath).replace(/\\/g, '/')),
+      // Prevents conflicts when multiple webpack runtimes (from different apps)
+      // are used on the same page.
+      jsonpFunction: `webpackJsonp${appPackageJson.name}`,
+      // this defaults to 'window', but by setting it to 'this' then
+      // module chunks which are built will work in web workers as well.
+      globalObject: 'this',
+    },
+    optimization: {
+      minimize: isEnvProduction,
+      minimizer: [
+        // This is only used in production mode
+        new TerserPlugin({
+          terserOptions: {
+            parse: {
+              // We want terser to parse ecma 8 code. However, we don't want it
+              // to apply any minification steps that turns valid ecma 5 code
+              // into invalid ecma 5 code. This is why the 'compress' and 'output'
+              // sections only apply transformations that are ecma 5 safe
+              // https://github.com/facebook/create-react-app/pull/4234
+              ecma: 8,
+            },
+            compress: {
+              ecma: 5,
+              warnings: false,
+              // Disabled because of an issue with Uglify breaking seemingly valid code:
+              // https://github.com/facebook/create-react-app/issues/2376
+              // Pending further investigation:
+              // https://github.com/mishoo/UglifyJS2/issues/2011
+              comparisons: false,
+              // Disabled because of an issue with Terser breaking valid code:
+              // https://github.com/facebook/create-react-app/issues/5250
+              // Pending further investigation:
+              // https://github.com/terser-js/terser/issues/120
+              inline: 2,
+            },
+            mangle: {
+              safari10: true,
+            },
+            // Added for profiling in devtools
+            keep_classnames: isEnvProductionProfile,
+            keep_fnames: isEnvProductionProfile,
+            output: {
+              ecma: 5,
+              comments: false,
+              // Turned on because emoji and regex is not minified properly using default
+              // https://github.com/facebook/create-react-app/issues/2488
+              ascii_only: true,
+            },
+          },
+          sourceMap: shouldUseSourceMap,
+        }),
+        // This is only used in production mode
+        new OptimizeCSSAssetsPlugin({
+          cssProcessorOptions: {
+            parser: safePostCssParser,
+            map: shouldUseSourceMap
+              ? {
+                  // `inline: false` forces the sourcemap to be output into a
+                  // separate file
+                  inline: false,
+                  // `annotation: true` appends the sourceMappingURL to the end of
+                  // the css file, helping the browser find the sourcemap
+                  annotation: true,
+                }
+              : false,
+          },
+          cssProcessorPluginOptions: {
+            preset: ['default', { minifyFontValues: { removeQuotes: false } }],
+          },
+        }),
+      ],
+      // Automatically split vendor and commons
+      // https://twitter.com/wSokra/status/969633336732905474
+      // https://medium.com/webpack/webpack-4-code-splitting-chunk-graph-and-the-splitchunks-optimization-be739a861366
+      splitChunks: {
+        chunks: 'all',
+        name: false,
+      },
+      // Keep the runtime chunk separated to enable long term caching
+      // https://twitter.com/wSokra/status/969679223278505985
+      // https://github.com/facebook/create-react-app/issues/5358
+      runtimeChunk: {
+        name: entrypoint => `runtime-${entrypoint.name}`,
+      },
+    },
+    resolve: {
+      // This allows you to set a fallback for where webpack should look for modules.
+      // We placed these paths second because we want `node_modules` to "win"
+      // if there are any conflicts. This matches Node resolution mechanism.
+      // https://github.com/facebook/create-react-app/issues/253
+      modules: ['node_modules', paths.appNodeModules].concat(
+        modules.additionalModulePaths || []
+      ),
+      // These are the reasonable defaults supported by the Node ecosystem.
+      // We also include JSX as a common component filename extension to support
+      // some tools, although we do not recommend using it, see:
+      // https://github.com/facebook/create-react-app/issues/290
+      // `web` extension prefixes have been added for better support
+      // for React Native Web.
+      extensions: paths.moduleFileExtensions
+        .map(ext => `.${ext}`)
+        .filter(ext => useTypeScript || !ext.includes('ts')),
+      alias: {
+        // Support React Native Web
+        // https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/
+        'react-native': 'react-native-web',
+        // Allows for better profiling with ReactDevTools
+        ...(isEnvProductionProfile && {
+          'react-dom$': 'react-dom/profiling',
+          'scheduler/tracing': 'scheduler/tracing-profiling',
+        }),
+        ...(modules.webpackAliases || {}),
+      },
+      plugins: [
+        // Adds support for installing with Plug'n'Play, leading to faster installs and adding
+        // guards against forgotten dependencies and such.
+        PnpWebpackPlugin,
+        // Prevents users from importing files from outside of src/ (or node_modules/).
+        // This often causes confusion because we only process files within src/ with babel.
+        // To fix this, we prevent you from importing files out of src/ -- if you'd like to,
+        // please link the files into your node_modules/ and let module-resolution kick in.
+        // Make sure your source files are compiled, as they will not be processed in any way.
+        new ModuleScopePlugin(paths.appSrc, [paths.appPackageJson]),
+      ],
+    },
+    resolveLoader: {
+      plugins: [
+        // Also related to Plug'n'Play, but this time it tells webpack to load its loaders
+        // from the current package.
+        PnpWebpackPlugin.moduleLoader(module),
+      ],
+    },
+    module: {
+      strictExportPresence: true,
+      rules: [
+        // Disable require.ensure as it's not a standard language feature.
+        { parser: { requireEnsure: false } },
+
+        // First, run the linter.
+        // It's important to do this before Babel processes the JS.
+        {
+          test: /\.(js|mjs|jsx|ts|tsx)$/,
+          enforce: 'pre',
+          use: [
+            {
+              options: {
+                cache: true,
+                formatter: require.resolve('react-dev-utils/eslintFormatter'),
+                eslintPath: require.resolve('eslint'),
+                resolvePluginsRelativeTo: __dirname,
+                
+              },
+              loader: require.resolve('eslint-loader'),
+            },
+          ],
+          include: paths.appSrc,
+        },
+        {
+          // "oneOf" will traverse all following loaders until one will
+          // match the requirements. When no loader matches it will fall
+          // back to the "file" loader at the end of the loader list.
+          oneOf: [
+            // "url" loader works like "file" loader except that it embeds assets
+            // smaller than specified limit in bytes as data URLs to avoid requests.
+            // A missing `test` is equivalent to a match.
+            {
+              test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
+              loader: require.resolve('url-loader'),
+              options: {
+                limit: imageInlineSizeLimit,
+                name: 'static/media/[name].[hash:8].[ext]',
+              },
+            },
+            // Process application JS with Babel.
+            // The preset includes JSX, Flow, TypeScript, and some ESnext features.
+            {
+              test: /\.(js|mjs|jsx|ts|tsx)$/,
+              include: paths.appSrc,
+              loader: require.resolve('babel-loader'),
+              options: {
+                customize: require.resolve(
+                  'babel-preset-react-app/webpack-overrides'
+                ),
+                
+                plugins: [
+                  [
+                    require.resolve('babel-plugin-named-asset-import'),
+                    {
+                      loaderMap: {
+                        svg: {
+                          ReactComponent:
+                            '@svgr/webpack?-svgo,+titleProp,+ref![path]',
+                        },
+                      },
+                    },
+                  ],
+                ],
+                // This is a feature of `babel-loader` for webpack (not Babel itself).
+                // It enables caching results in ./node_modules/.cache/babel-loader/
+                // directory for faster rebuilds.
+                cacheDirectory: true,
+                // See #6846 for context on why cacheCompression is disabled
+                cacheCompression: false,
+                compact: isEnvProduction,
+              },
+            },
+            // Process any JS outside of the app with Babel.
+            // Unlike the application JS, we only compile the standard ES features.
+            {
+              test: /\.(js|mjs)$/,
+              exclude: /@babel(?:\/|\\{1,2})runtime/,
+              loader: require.resolve('babel-loader'),
+              options: {
+                babelrc: false,
+                configFile: false,
+                compact: false,
+                presets: [
+                  [
+                    require.resolve('babel-preset-react-app/dependencies'),
+                    { helpers: true },
+                  ],
+                ],
+                cacheDirectory: true,
+                // See #6846 for context on why cacheCompression is disabled
+                cacheCompression: false,
+                
+                // Babel sourcemaps are needed for debugging into node_modules
+                // code.  Without the options below, debuggers like VSCode
+                // show incorrect code and set breakpoints on the wrong lines.
+                sourceMaps: shouldUseSourceMap,
+                inputSourceMap: shouldUseSourceMap,
+              },
+            },
+            // "postcss" loader applies autoprefixer to our CSS.
+            // "css" loader resolves paths in CSS and adds assets as dependencies.
+            // "style" loader turns CSS into JS modules that inject <style> tags.
+            // In production, we use MiniCSSExtractPlugin to extract that CSS
+            // to a file, but in development "style" loader enables hot editing
+            // of CSS.
+            // By default we support CSS Modules with the extension .module.css
+            {
+              test: cssRegex,
+              exclude: cssModuleRegex,
+              use: getStyleLoaders({
+                importLoaders: 1,
+                sourceMap: isEnvProduction && shouldUseSourceMap,
+              }),
+              // Don't consider CSS imports dead code even if the
+              // containing package claims to have no side effects.
+              // Remove this when webpack adds a warning or an error for this.
+              // See https://github.com/webpack/webpack/issues/6571
+              sideEffects: true,
+            },
+            // Adds support for CSS Modules (https://github.com/css-modules/css-modules)
+            // using the extension .module.css
+            {
+              test: cssModuleRegex,
+              use: getStyleLoaders({
+                importLoaders: 1,
+                sourceMap: isEnvProduction && shouldUseSourceMap,
+                modules: {
+                  getLocalIdent: getCSSModuleLocalIdent,
+                },
+              }),
+            },
+            // Opt-in support for SASS (using .scss or .sass extensions).
+            // By default we support SASS Modules with the
+            // extensions .module.scss or .module.sass
+            {
+              test: sassRegex,
+              exclude: sassModuleRegex,
+              use: getStyleLoaders(
+                {
+                  importLoaders: 3,
+                  sourceMap: isEnvProduction && shouldUseSourceMap,
+                },
+                'sass-loader'
+              ),
+              // Don't consider CSS imports dead code even if the
+              // containing package claims to have no side effects.
+              // Remove this when webpack adds a warning or an error for this.
+              // See https://github.com/webpack/webpack/issues/6571
+              sideEffects: true,
+            },
+            // Adds support for CSS Modules, but using SASS
+            // using the extension .module.scss or .module.sass
+            {
+              test: sassModuleRegex,
+              use: getStyleLoaders(
+                {
+                  importLoaders: 3,
+                  sourceMap: isEnvProduction && shouldUseSourceMap,
+                  modules: {
+                    getLocalIdent: getCSSModuleLocalIdent,
+                  },
+                },
+                'sass-loader'
+              ),
+            },
+            // "file" loader makes sure those assets get served by WebpackDevServer.
+            // When you `import` an asset, you get its (virtual) filename.
+            // In production, they would get copied to the `build` folder.
+            // This loader doesn't use a "test" so it will catch all modules
+            // that fall through the other loaders.
+            {
+              loader: require.resolve('file-loader'),
+              // Exclude `js` files to keep "css" loader working as it injects
+              // its runtime that would otherwise be processed through "file" loader.
+              // Also exclude `html` and `json` extensions so they get processed
+              // by webpacks internal loaders.
+              exclude: [/\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/],
+              options: {
+                name: 'static/media/[name].[hash:8].[ext]',
+              },
+            },
+            // ** STOP ** Are you adding a new loader?
+            // Make sure to add the new loader(s) before the "file" loader.
+          ],
+        },
+      ],
+    },
+    plugins: [
+      // Generates an `index.html` file with the <script> injected.
+      new HtmlWebpackPlugin(
+        Object.assign(
+          {},
+          {
+            inject: true,
+            template: paths.appHtml,
+          },
+          isEnvProduction
+            ? {
+                minify: {
+                  removeComments: true,
+                  collapseWhitespace: true,
+                  removeRedundantAttributes: true,
+                  useShortDoctype: true,
+                  removeEmptyAttributes: true,
+                  removeStyleLinkTypeAttributes: true,
+                  keepClosingSlash: true,
+                  minifyJS: true,
+                  minifyCSS: true,
+                  minifyURLs: true,
+                },
+              }
+            : undefined
+        )
+      ),
+      // Inlines the webpack runtime script. This script is too small to warrant
+      // a network request.
+      // https://github.com/facebook/create-react-app/issues/5358
+      isEnvProduction &&
+        shouldInlineRuntimeChunk &&
+        new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/runtime-.+[.]js/]),
+      // Makes some environment variables available in index.html.
+      // The public URL is available as %PUBLIC_URL% in index.html, e.g.:
+      // <link rel="icon" href="%PUBLIC_URL%/favicon.ico">
+      // It will be an empty string unless you specify "homepage"
+      // in `package.json`, in which case it will be the pathname of that URL.
+      new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw),
+      // This gives some necessary context to module not found errors, such as
+      // the requesting resource.
+      new ModuleNotFoundPlugin(paths.appPath),
+      // Makes some environment variables available to the JS code, for example:
+      // if (process.env.NODE_ENV === 'production') { ... }. See `./env.js`.
+      // It is absolutely essential that NODE_ENV is set to production
+      // during a production build.
+      // Otherwise React will be compiled in the very slow development mode.
+      new webpack.DefinePlugin(env.stringified),
+      // This is necessary to emit hot updates (currently CSS only):
+      isEnvDevelopment && new webpack.HotModuleReplacementPlugin(),
+      // Watcher doesn't work well if you mistype casing in a path so we use
+      // a plugin that prints an error when you attempt to do this.
+      // See https://github.com/facebook/create-react-app/issues/240
+      isEnvDevelopment && new CaseSensitivePathsPlugin(),
+      // If you require a missing module and then `npm install` it, you still have
+      // to restart the development server for webpack to discover it. This plugin
+      // makes the discovery automatic so you don't have to restart.
+      // See https://github.com/facebook/create-react-app/issues/186
+      isEnvDevelopment &&
+        new WatchMissingNodeModulesPlugin(paths.appNodeModules),
+      isEnvProduction &&
+        new MiniCssExtractPlugin({
+          // Options similar to the same options in webpackOptions.output
+          // both options are optional
+          filename: 'static/css/[name].[contenthash:8].css',
+          chunkFilename: 'static/css/[name].[contenthash:8].chunk.css',
+        }),
+      // Generate an asset manifest file with the following content:
+      // - "files" key: Mapping of all asset filenames to their corresponding
+      //   output file so that tools can pick it up without having to parse
+      //   `index.html`
+      // - "entrypoints" key: Array of files which are included in `index.html`,
+      //   can be used to reconstruct the HTML if necessary
+      new ManifestPlugin({
+        fileName: 'asset-manifest.json',
+        publicPath: paths.publicUrlOrPath,
+        generate: (seed, files, entrypoints) => {
+          const manifestFiles = files.reduce((manifest, file) => {
+            manifest[file.name] = file.path;
+            return manifest;
+          }, seed);
+          const entrypointFiles = entrypoints.main.filter(
+            fileName => !fileName.endsWith('.map')
+          );
+
+          return {
+            files: manifestFiles,
+            entrypoints: entrypointFiles,
+          };
+        },
+      }),
+      // Moment.js is an extremely popular library that bundles large locale files
+      // by default due to how webpack interprets its code. This is a practical
+      // solution that requires the user to opt into importing specific locales.
+      // https://github.com/jmblog/how-to-optimize-momentjs-with-webpack
+      // You can remove this if you don't use Moment.js:
+      new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
+      // Generate a service worker script that will precache, and keep up to date,
+      // the HTML & assets that are part of the webpack build.
+      isEnvProduction &&
+        new WorkboxWebpackPlugin.GenerateSW({
+          clientsClaim: true,
+          exclude: [/\.map$/, /asset-manifest\.json$/],
+          importWorkboxFrom: 'cdn',
+          navigateFallback: paths.publicUrlOrPath + 'index.html',
+          navigateFallbackBlacklist: [
+            // Exclude URLs starting with /_, as they're likely an API call
+            new RegExp('^/_'),
+            // Exclude any URLs whose last part seems to be a file extension
+            // as they're likely a resource and not a SPA route.
+            // URLs containing a "?" character won't be blacklisted as they're likely
+            // a route with query params (e.g. auth callbacks).
+            new RegExp('/[^/?]+\\.[^/]+$'),
+          ],
+        }),
+      // TypeScript type checking
+      useTypeScript &&
+        new ForkTsCheckerWebpackPlugin({
+          typescript: resolve.sync('typescript', {
+            basedir: paths.appNodeModules,
+          }),
+          async: isEnvDevelopment,
+          useTypescriptIncrementalApi: true,
+          checkSyntacticErrors: true,
+          resolveModuleNameModule: process.versions.pnp
+            ? `${__dirname}/pnpTs.js`
+            : undefined,
+          resolveTypeReferenceDirectiveModule: process.versions.pnp
+            ? `${__dirname}/pnpTs.js`
+            : undefined,
+          tsconfig: paths.appTsConfig,
+          reportFiles: [
+            '**',
+            '!**/__tests__/**',
+            '!**/?(*.)(spec|test).*',
+            '!**/src/setupProxy.*',
+            '!**/src/setupTests.*',
+          ],
+          silent: true,
+          // The formatter is invoked directly in WebpackDevServerUtils during development
+          formatter: isEnvProduction ? typescriptFormatter : undefined,
+        }),
+    ].filter(Boolean),
+    // Some libraries import Node modules but don't use them in the browser.
+    // Tell webpack to provide empty mocks for them so importing them works.
+    node: {
+      module: 'empty',
+      dgram: 'empty',
+      dns: 'mock',
+      fs: 'empty',
+      http2: 'empty',
+      net: 'empty',
+      tls: 'empty',
+      child_process: 'empty',
+    },
+    // Turn off performance processing because we utilize
+    // our own hints via the FileSizeReporter
+    performance: false,
+  };
+};

+ 130 - 0
frontend/chuchufe/config/webpackDevServer.config.js

@@ -0,0 +1,130 @@
+'use strict';
+
+const fs = require('fs');
+const errorOverlayMiddleware = require('react-dev-utils/errorOverlayMiddleware');
+const evalSourceMapMiddleware = require('react-dev-utils/evalSourceMapMiddleware');
+const noopServiceWorkerMiddleware = require('react-dev-utils/noopServiceWorkerMiddleware');
+const ignoredFiles = require('react-dev-utils/ignoredFiles');
+const redirectServedPath = require('react-dev-utils/redirectServedPathMiddleware');
+const paths = require('./paths');
+const getHttpsConfig = require('./getHttpsConfig');
+
+const host = process.env.HOST || '0.0.0.0';
+const sockHost = process.env.WDS_SOCKET_HOST;
+const sockPath = process.env.WDS_SOCKET_PATH; // default: '/sockjs-node'
+const sockPort = process.env.WDS_SOCKET_PORT;
+
+module.exports = function(proxy, allowedHost) {
+  return {
+    // WebpackDevServer 2.4.3 introduced a security fix that prevents remote
+    // websites from potentially accessing local content through DNS rebinding:
+    // https://github.com/webpack/webpack-dev-server/issues/887
+    // https://medium.com/webpack/webpack-dev-server-middleware-security-issues-1489d950874a
+    // However, it made several existing use cases such as development in cloud
+    // environment or subdomains in development significantly more complicated:
+    // https://github.com/facebook/create-react-app/issues/2271
+    // https://github.com/facebook/create-react-app/issues/2233
+    // While we're investigating better solutions, for now we will take a
+    // compromise. Since our WDS configuration only serves files in the `public`
+    // folder we won't consider accessing them a vulnerability. However, if you
+    // use the `proxy` feature, it gets more dangerous because it can expose
+    // remote code execution vulnerabilities in backends like Django and Rails.
+    // So we will disable the host check normally, but enable it if you have
+    // specified the `proxy` setting. Finally, we let you override it if you
+    // really know what you're doing with a special environment variable.
+    disableHostCheck:
+      !proxy || process.env.DANGEROUSLY_DISABLE_HOST_CHECK === 'true',
+    // Enable gzip compression of generated files.
+    compress: true,
+    // Silence WebpackDevServer's own logs since they're generally not useful.
+    // It will still show compile warnings and errors with this setting.
+    clientLogLevel: 'none',
+    // By default WebpackDevServer serves physical files from current directory
+    // in addition to all the virtual build products that it serves from memory.
+    // This is confusing because those files won’t automatically be available in
+    // production build folder unless we copy them. However, copying the whole
+    // project directory is dangerous because we may expose sensitive files.
+    // Instead, we establish a convention that only files in `public` directory
+    // get served. Our build script will copy `public` into the `build` folder.
+    // In `index.html`, you can get URL of `public` folder with %PUBLIC_URL%:
+    // <link rel="icon" href="%PUBLIC_URL%/favicon.ico">
+    // In JavaScript code, you can access it with `process.env.PUBLIC_URL`.
+    // Note that we only recommend to use `public` folder as an escape hatch
+    // for files like `favicon.ico`, `manifest.json`, and libraries that are
+    // for some reason broken when imported through webpack. If you just want to
+    // use an image, put it in `src` and `import` it from JavaScript instead.
+    contentBase: paths.appPublic,
+    contentBasePublicPath: paths.publicUrlOrPath,
+    // By default files from `contentBase` will not trigger a page reload.
+    watchContentBase: true,
+    // Enable hot reloading server. It will provide WDS_SOCKET_PATH endpoint
+    // for the WebpackDevServer client so it can learn when the files were
+    // updated. The WebpackDevServer client is included as an entry point
+    // in the webpack development configuration. Note that only changes
+    // to CSS are currently hot reloaded. JS changes will refresh the browser.
+    hot: true,
+    // Use 'ws' instead of 'sockjs-node' on server since we're using native
+    // websockets in `webpackHotDevClient`.
+    transportMode: 'ws',
+    // Prevent a WS client from getting injected as we're already including
+    // `webpackHotDevClient`.
+    injectClient: false,
+    // Enable custom sockjs pathname for websocket connection to hot reloading server.
+    // Enable custom sockjs hostname, pathname and port for websocket connection
+    // to hot reloading server.
+    sockHost,
+    sockPath,
+    sockPort,
+    // It is important to tell WebpackDevServer to use the same "publicPath" path as
+    // we specified in the webpack config. When homepage is '.', default to serving
+    // from the root.
+    // remove last slash so user can land on `/test` instead of `/test/`
+    publicPath: paths.publicUrlOrPath.slice(0, -1),
+    // WebpackDevServer is noisy by default so we emit custom message instead
+    // by listening to the compiler events with `compiler.hooks[...].tap` calls above.
+    quiet: true,
+    // Reportedly, this avoids CPU overload on some systems.
+    // https://github.com/facebook/create-react-app/issues/293
+    // src/node_modules is not ignored to support absolute imports
+    // https://github.com/facebook/create-react-app/issues/1065
+    watchOptions: {
+      ignored: ignoredFiles(paths.appSrc),
+    },
+    https: getHttpsConfig(),
+    host,
+    overlay: false,
+    historyApiFallback: {
+      // Paths with dots should still use the history fallback.
+      // See https://github.com/facebook/create-react-app/issues/387.
+      disableDotRule: true,
+      index: paths.publicUrlOrPath,
+    },
+    public: allowedHost,
+    // `proxy` is run between `before` and `after` `webpack-dev-server` hooks
+    proxy,
+    before(app, server) {
+      // Keep `evalSourceMapMiddleware` and `errorOverlayMiddleware`
+      // middlewares before `redirectServedPath` otherwise will not have any effect
+      // This lets us fetch source contents from webpack for the error overlay
+      app.use(evalSourceMapMiddleware(server));
+      // This lets us open files from the runtime error overlay.
+      app.use(errorOverlayMiddleware());
+
+      if (fs.existsSync(paths.proxySetup)) {
+        // This registers user provided middleware for proxy reasons
+        require(paths.proxySetup)(app);
+      }
+    },
+    after(app) {
+      // Redirect to `PUBLIC_URL` or `homepage` from `package.json` if url not match
+      app.use(redirectServedPath(paths.publicUrlOrPath));
+
+      // This service worker file is effectively a 'no-op' that will reset any
+      // previous service worker registered for the same host:port combination.
+      // We do this in development to avoid hitting the production cache if
+      // it used the same host and port.
+      // https://github.com/facebook/create-react-app/issues/2272#issuecomment-302832432
+      app.use(noopServiceWorkerMiddleware(paths.publicUrlOrPath));
+    },
+  };
+};

+ 110 - 5
frontend/chuchufe/package.json

@@ -3,18 +3,68 @@
   "version": "0.1.0",
   "private": true,
   "dependencies": {
+    "@babel/core": "7.9.0",
+    "@svgr/webpack": "4.3.3",
     "@testing-library/jest-dom": "^4.2.4",
     "@testing-library/react": "^9.3.2",
     "@testing-library/user-event": "^7.1.2",
+    "@typescript-eslint/eslint-plugin": "^2.10.0",
+    "@typescript-eslint/parser": "^2.10.0",
+    "babel-eslint": "10.1.0",
+    "babel-jest": "^24.9.0",
+    "babel-loader": "8.1.0",
+    "babel-plugin-named-asset-import": "^0.3.6",
+    "babel-preset-react-app": "^9.1.2",
+    "camelcase": "^5.3.1",
+    "case-sensitive-paths-webpack-plugin": "2.3.0",
+    "css-loader": "3.4.2",
+    "dotenv": "8.2.0",
+    "dotenv-expand": "5.1.0",
+    "eslint": "^6.6.0",
+    "eslint-config-react-app": "^5.2.1",
+    "eslint-loader": "3.0.3",
+    "eslint-plugin-flowtype": "4.6.0",
+    "eslint-plugin-import": "2.20.1",
+    "eslint-plugin-jsx-a11y": "6.2.3",
+    "eslint-plugin-react": "7.19.0",
+    "eslint-plugin-react-hooks": "^1.6.1",
+    "file-loader": "4.3.0",
+    "fs-extra": "^8.1.0",
+    "html-webpack-plugin": "4.0.0-beta.11",
+    "identity-obj-proxy": "3.0.0",
+    "jest": "24.9.0",
+    "jest-environment-jsdom-fourteen": "1.0.1",
+    "jest-resolve": "24.9.0",
+    "jest-watch-typeahead": "0.4.2",
+    "mini-css-extract-plugin": "0.9.0",
+    "optimize-css-assets-webpack-plugin": "5.0.3",
+    "pnp-webpack-plugin": "1.6.4",
+    "postcss-flexbugs-fixes": "4.1.0",
+    "postcss-loader": "3.0.0",
+    "postcss-normalize": "8.0.1",
+    "postcss-preset-env": "6.7.0",
+    "postcss-safe-parser": "4.0.1",
     "react": "^16.14.0",
+    "react-app-polyfill": "^1.0.6",
+    "react-dev-utils": "^10.2.1",
     "react-dom": "^16.14.0",
-    "react-scripts": "3.4.3"
+    "resolve": "1.15.0",
+    "resolve-url-loader": "3.1.1",
+    "sass-loader": "8.0.2",
+    "semver": "6.3.0",
+    "style-loader": "0.23.1",
+    "terser-webpack-plugin": "2.3.8",
+    "ts-pnp": "1.1.6",
+    "url-loader": "2.3.0",
+    "webpack": "4.42.0",
+    "webpack-dev-server": "3.11.0",
+    "webpack-manifest-plugin": "2.2.0",
+    "workbox-webpack-plugin": "4.3.1"
   },
   "scripts": {
-    "start": "react-scripts start",
-    "build": "react-scripts build",
-    "test": "react-scripts test",
-    "eject": "react-scripts eject"
+    "start": "node scripts/start.js",
+    "build": "node scripts/build.js",
+    "test": "node scripts/test.js"
   },
   "eslintConfig": {
     "extends": "react-app"
@@ -30,5 +80,60 @@
       "last 1 firefox version",
       "last 1 safari version"
     ]
+  },
+  "jest": {
+    "roots": [
+      "<rootDir>/src"
+    ],
+    "collectCoverageFrom": [
+      "src/**/*.{js,jsx,ts,tsx}",
+      "!src/**/*.d.ts"
+    ],
+    "setupFiles": [
+      "react-app-polyfill/jsdom"
+    ],
+    "setupFilesAfterEnv": [
+      "<rootDir>/src/setupTests.js"
+    ],
+    "testMatch": [
+      "<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}",
+      "<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}"
+    ],
+    "testEnvironment": "jest-environment-jsdom-fourteen",
+    "transform": {
+      "^.+\\.(js|jsx|ts|tsx)$": "<rootDir>/node_modules/babel-jest",
+      "^.+\\.css$": "<rootDir>/config/jest/cssTransform.js",
+      "^(?!.*\\.(js|jsx|ts|tsx|css|json)$)": "<rootDir>/config/jest/fileTransform.js"
+    },
+    "transformIgnorePatterns": [
+      "[/\\\\]node_modules[/\\\\].+\\.(js|jsx|ts|tsx)$",
+      "^.+\\.module\\.(css|sass|scss)$"
+    ],
+    "modulePaths": [],
+    "moduleNameMapper": {
+      "^react-native$": "react-native-web",
+      "^.+\\.module\\.(css|sass|scss)$": "identity-obj-proxy"
+    },
+    "moduleFileExtensions": [
+      "web.js",
+      "js",
+      "web.ts",
+      "ts",
+      "web.tsx",
+      "tsx",
+      "json",
+      "web.jsx",
+      "jsx",
+      "node"
+    ],
+    "watchPlugins": [
+      "jest-watch-typeahead/filename",
+      "jest-watch-typeahead/testname"
+    ]
+  },
+  "babel": {
+    "presets": [
+      "react-app"
+    ]
   }
 }

+ 38 - 40
frontend/chuchufe/public/index.html

@@ -1,45 +1,43 @@
-<!doctype html>
+<!DOCTYPE html>
 <html lang="en">
+  <head>
+    <meta charset="utf-8" />
+<!--    <link rel="icon" href="public/favicon.ico" />-->
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
+    <meta name="theme-color" content="#000000" />
+    <meta
+      name="description"
+      content="Web site created using create-react-app"
+    />
+<!--    <link rel="apple-touch-icon" href="public/logo192.png" />-->
+    <!--
+      manifest.json provides metadata used when your web app is installed on a
+      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
+    -->
+    <link rel="manifest" href="public/manifest.json" />
+    <!--
+      Notice the use of %PUBLIC_URL% in the tags above.
+      It will be replaced with the URL of the `public` folder during the build.
+      Only files inside the `public` folder can be referenced from the HTML.
 
-<head>
-    <meta charset="utf-8">
-    <title>ChuChube</title>
-    <meta name="description" content="">
-    <meta name="viewport" content="width=device-width, initial-scale=1">
+      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
+      work correctly both with client-side routing and a non-root public URL.
+      Learn how to configure a non-root public URL by running `npm run build`.
+    -->
+    <title>React App</title>
+  </head>
+  <body>
+    <noscript>You need to enable JavaScript to run this app.</noscript>
+    <div id="root"></div>
+    <!--
+      This HTML file is a template.
+      If you open it directly in the browser, you will see an empty page.
 
-    <meta property="og:title" content="">
-    <meta property="og:type" content="">
-    <meta property="og:url" content="">
-    <meta property="og:image" content="">
-
-<!--    <link rel="icon" type="image/png" href="favicon.png">-->
-    <link rel="stylesheet" href="css/normalize.css">
-    <link rel="stylesheet" href="css/fontawesome.css">
-    <link rel="stylesheet" href="css/solid.css">
-    <link rel="stylesheet" href="css/bootstrap/bootstrap.css">
-    <link rel="stylesheet" href="css/main.css">
-
-    <meta name="theme-color" content="#fafafa">
-</head>
-
-<body>
-<div class="container absoluteCenter" style="height: 135px">
-    <div style="margin-top: -100px" >
-        <div class="row mb-4">
-            <img class="mx-auto" alt="ChuChube logo" src="img/logo-100.png"/>
-        </div>
-        <div class="row">
-            <form class="mx-auto" id="roomInputForm">
-                <div class="form-group">
-                    <label class="sr-only" for="roomInput">Room Input</label>
-                    <input class="form-control" id="roomInput" placeholder="Room" autofocus/>
-                </div>
-            </form>
-        </div>
-    </div>
-</div>
-<footer class="footer mb-4"><a href="https://github.com/NielsOverkamp/ChuChube">Made by Niels Overkamp</a>&nbsp·&nbsp<a href="http://joetoep.student.utwente.nl">Based on JoeToep</a></footer>
-<script src="index.js" type="module"></script>
-</body>
+      You can add webfonts, meta tags, or analytics to this file.
+      The build step will place the bundled scripts into the <body> tag.
 
+      To begin the development, run `npm start` or `yarn start`.
+      To create a production bundle, use `npm run build` or `yarn build`.
+    -->
+  </body>
 </html>

+ 0 - 43
frontend/chuchufe/public/player.html

@@ -1,43 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-  <head>
-    <meta charset="utf-8" />
-<!--    <link rel="icon" href="public/favicon.ico" />-->
-    <meta name="viewport" content="width=device-width, initial-scale=1" />
-    <meta name="theme-color" content="#000000" />
-    <meta
-      name="description"
-      content="Web site created using create-react-app"
-    />
-<!--    <link rel="apple-touch-icon" href="public/logo192.png" />-->
-    <!--
-      manifest.json provides metadata used when your web app is installed on a
-      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-    -->
-    <link rel="manifest" href="public/manifest.json" />
-    <!--
-      Notice the use of %PUBLIC_URL% in the tags above.
-      It will be replaced with the URL of the `public` folder during the build.
-      Only files inside the `public` folder can be referenced from the HTML.
-
-      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
-      work correctly both with client-side routing and a non-root public URL.
-      Learn how to configure a non-root public URL by running `npm run build`.
-    -->
-    <title>React App</title>
-  </head>
-  <body>
-    <noscript>You need to enable JavaScript to run this app.</noscript>
-    <div id="root"></div>
-    <!--
-      This HTML file is a template.
-      If you open it directly in the browser, you will see an empty page.
-
-      You can add webfonts, meta tags, or analytics to this file.
-      The build step will place the bundled scripts into the <body> tag.
-
-      To begin the development, run `npm start` or `yarn start`.
-      To create a production bundle, use `npm run build` or `yarn build`.
-    -->
-  </body>
-</html>

+ 211 - 0
frontend/chuchufe/scripts/build.js

@@ -0,0 +1,211 @@
+'use strict';
+
+// Do this as the first thing so that any code reading it knows the right env.
+process.env.BABEL_ENV = 'production';
+process.env.NODE_ENV = 'production';
+
+// Makes the script crash on unhandled rejections instead of silently
+// ignoring them. In the future, promise rejections that are not handled will
+// terminate the Node.js process with a non-zero exit code.
+process.on('unhandledRejection', err => {
+  throw err;
+});
+
+// Ensure environment variables are read.
+require('../config/env');
+
+
+const path = require('path');
+const chalk = require('react-dev-utils/chalk');
+const fs = require('fs-extra');
+const webpack = require('webpack');
+const configFactory = require('../config/webpack.config');
+const paths = require('../config/paths');
+const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
+const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages');
+const printHostingInstructions = require('react-dev-utils/printHostingInstructions');
+const FileSizeReporter = require('react-dev-utils/FileSizeReporter');
+const printBuildError = require('react-dev-utils/printBuildError');
+
+const measureFileSizesBeforeBuild =
+  FileSizeReporter.measureFileSizesBeforeBuild;
+const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild;
+const useYarn = fs.existsSync(paths.yarnLockFile);
+
+// These sizes are pretty large. We'll warn for bundles exceeding them.
+const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024;
+const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024;
+
+const isInteractive = process.stdout.isTTY;
+
+// Warn and crash if required files are missing
+if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
+  process.exit(1);
+}
+
+// Generate configuration
+const config = configFactory('production');
+
+// We require that you explicitly set browsers and do not fall back to
+// browserslist defaults.
+const { checkBrowsers } = require('react-dev-utils/browsersHelper');
+checkBrowsers(paths.appPath, isInteractive)
+  .then(() => {
+    // First, read the current file sizes in build directory.
+    // This lets us display how much they changed later.
+    return measureFileSizesBeforeBuild(paths.appBuild);
+  })
+  .then(previousFileSizes => {
+    // Remove all content but keep the directory so that
+    // if you're in it, you don't end up in Trash
+    fs.emptyDirSync(paths.appBuild);
+    // Merge with the public folder
+    copyPublicFolder();
+    // Start the webpack build
+    return build(previousFileSizes);
+  })
+  .then(
+    ({ stats, previousFileSizes, warnings }) => {
+      if (warnings.length) {
+        console.log(chalk.yellow('Compiled with warnings.\n'));
+        console.log(warnings.join('\n\n'));
+        console.log(
+          '\nSearch for the ' +
+            chalk.underline(chalk.yellow('keywords')) +
+            ' to learn more about each warning.'
+        );
+        console.log(
+          'To ignore, add ' +
+            chalk.cyan('// eslint-disable-next-line') +
+            ' to the line before.\n'
+        );
+      } else {
+        console.log(chalk.green('Compiled successfully.\n'));
+      }
+
+      console.log('File sizes after gzip:\n');
+      printFileSizesAfterBuild(
+        stats,
+        previousFileSizes,
+        paths.appBuild,
+        WARN_AFTER_BUNDLE_GZIP_SIZE,
+        WARN_AFTER_CHUNK_GZIP_SIZE
+      );
+      console.log();
+
+      const appPackage = require(paths.appPackageJson);
+      const publicUrl = paths.publicUrlOrPath;
+      const publicPath = config.output.publicPath;
+      const buildFolder = path.relative(process.cwd(), paths.appBuild);
+      printHostingInstructions(
+        appPackage,
+        publicUrl,
+        publicPath,
+        buildFolder,
+        useYarn
+      );
+    },
+    err => {
+      const tscCompileOnError = process.env.TSC_COMPILE_ON_ERROR === 'true';
+      if (tscCompileOnError) {
+        console.log(
+          chalk.yellow(
+            'Compiled with the following type errors (you may want to check these before deploying your app):\n'
+          )
+        );
+        printBuildError(err);
+      } else {
+        console.log(chalk.red('Failed to compile.\n'));
+        printBuildError(err);
+        process.exit(1);
+      }
+    }
+  )
+  .catch(err => {
+    if (err && err.message) {
+      console.log(err.message);
+    }
+    process.exit(1);
+  });
+
+// Create the production build and print the deployment instructions.
+function build(previousFileSizes) {
+  // We used to support resolving modules according to `NODE_PATH`.
+  // This now has been deprecated in favor of jsconfig/tsconfig.json
+  // This lets you use absolute paths in imports inside large monorepos:
+  if (process.env.NODE_PATH) {
+    console.log(
+      chalk.yellow(
+        'Setting NODE_PATH to resolve modules absolutely has been deprecated in favor of setting baseUrl in jsconfig.json (or tsconfig.json if you are using TypeScript) and will be removed in a future major release of create-react-app.'
+      )
+    );
+    console.log();
+  }
+
+  console.log('Creating an optimized production build...');
+
+  const compiler = webpack(config);
+  return new Promise((resolve, reject) => {
+    compiler.run((err, stats) => {
+      let messages;
+      if (err) {
+        if (!err.message) {
+          return reject(err);
+        }
+
+        let errMessage = err.message;
+
+        // Add additional information for postcss errors
+        if (Object.prototype.hasOwnProperty.call(err, 'postcssNode')) {
+          errMessage +=
+            '\nCompileError: Begins at CSS selector ' +
+            err['postcssNode'].selector;
+        }
+
+        messages = formatWebpackMessages({
+          errors: [errMessage],
+          warnings: [],
+        });
+      } else {
+        messages = formatWebpackMessages(
+          stats.toJson({ all: false, warnings: true, errors: true })
+        );
+      }
+      if (messages.errors.length) {
+        // Only keep the first error. Others are often indicative
+        // of the same problem, but confuse the reader with noise.
+        if (messages.errors.length > 1) {
+          messages.errors.length = 1;
+        }
+        return reject(new Error(messages.errors.join('\n\n')));
+      }
+      if (
+        process.env.CI &&
+        (typeof process.env.CI !== 'string' ||
+          process.env.CI.toLowerCase() !== 'false') &&
+        messages.warnings.length
+      ) {
+        console.log(
+          chalk.yellow(
+            '\nTreating warnings as errors because process.env.CI = true.\n' +
+              'Most CI servers set it automatically.\n'
+          )
+        );
+        return reject(new Error(messages.warnings.join('\n\n')));
+      }
+
+      return resolve({
+        stats,
+        previousFileSizes,
+        warnings: messages.warnings,
+      });
+    });
+  });
+}
+
+function copyPublicFolder() {
+  fs.copySync(paths.appPublic, paths.appBuild, {
+    dereference: true,
+    filter: file => file !== paths.appHtml,
+  });
+}

+ 166 - 0
frontend/chuchufe/scripts/start.js

@@ -0,0 +1,166 @@
+'use strict';
+
+// Do this as the first thing so that any code reading it knows the right env.
+process.env.BABEL_ENV = 'development';
+process.env.NODE_ENV = 'development';
+
+// Makes the script crash on unhandled rejections instead of silently
+// ignoring them. In the future, promise rejections that are not handled will
+// terminate the Node.js process with a non-zero exit code.
+process.on('unhandledRejection', err => {
+  throw err;
+});
+
+// Ensure environment variables are read.
+require('../config/env');
+
+
+const fs = require('fs');
+const chalk = require('react-dev-utils/chalk');
+const webpack = require('webpack');
+const WebpackDevServer = require('webpack-dev-server');
+const clearConsole = require('react-dev-utils/clearConsole');
+const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
+const {
+  choosePort,
+  createCompiler,
+  prepareProxy,
+  prepareUrls,
+} = require('react-dev-utils/WebpackDevServerUtils');
+const openBrowser = require('react-dev-utils/openBrowser');
+const paths = require('../config/paths');
+const configFactory = require('../config/webpack.config');
+const createDevServerConfig = require('../config/webpackDevServer.config');
+
+const useYarn = fs.existsSync(paths.yarnLockFile);
+const isInteractive = process.stdout.isTTY;
+
+// Warn and crash if required files are missing
+if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
+  process.exit(1);
+}
+
+// Tools like Cloud9 rely on this.
+const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000;
+const HOST = process.env.HOST || '0.0.0.0';
+
+if (process.env.HOST) {
+  console.log(
+    chalk.cyan(
+      `Attempting to bind to HOST environment variable: ${chalk.yellow(
+        chalk.bold(process.env.HOST)
+      )}`
+    )
+  );
+  console.log(
+    `If this was unintentional, check that you haven't mistakenly set it in your shell.`
+  );
+  console.log(
+    `Learn more here: ${chalk.yellow('https://bit.ly/CRA-advanced-config')}`
+  );
+  console.log();
+}
+
+// We require that you explicitly set browsers and do not fall back to
+// browserslist defaults.
+const { checkBrowsers } = require('react-dev-utils/browsersHelper');
+checkBrowsers(paths.appPath, isInteractive)
+  .then(() => {
+    // We attempt to use the default port but if it is busy, we offer the user to
+    // run on a different port. `choosePort()` Promise resolves to the next free port.
+    return choosePort(HOST, DEFAULT_PORT);
+  })
+  .then(port => {
+    if (port == null) {
+      // We have not found a port.
+      return;
+    }
+
+    const config = configFactory('development');
+    const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
+    const appName = require(paths.appPackageJson).name;
+    const useTypeScript = fs.existsSync(paths.appTsConfig);
+    const tscCompileOnError = process.env.TSC_COMPILE_ON_ERROR === 'true';
+    const urls = prepareUrls(
+      protocol,
+      HOST,
+      port,
+      paths.publicUrlOrPath.slice(0, -1)
+    );
+    const devSocket = {
+      warnings: warnings =>
+        devServer.sockWrite(devServer.sockets, 'warnings', warnings),
+      errors: errors =>
+        devServer.sockWrite(devServer.sockets, 'errors', errors),
+    };
+    // Create a webpack compiler that is configured with custom messages.
+    const compiler = createCompiler({
+      appName,
+      config,
+      devSocket,
+      urls,
+      useYarn,
+      useTypeScript,
+      tscCompileOnError,
+      webpack,
+    });
+    // Load proxy config
+    const proxySetting = require(paths.appPackageJson).proxy;
+    const proxyConfig = prepareProxy(
+      proxySetting,
+      paths.appPublic,
+      paths.publicUrlOrPath
+    );
+    // Serve webpack assets generated by the compiler over a web server.
+    const serverConfig = createDevServerConfig(
+      proxyConfig,
+      urls.lanUrlForConfig
+    );
+    const devServer = new WebpackDevServer(compiler, serverConfig);
+    // Launch WebpackDevServer.
+    devServer.listen(port, HOST, err => {
+      if (err) {
+        return console.log(err);
+      }
+      if (isInteractive) {
+        clearConsole();
+      }
+
+      // We used to support resolving modules according to `NODE_PATH`.
+      // This now has been deprecated in favor of jsconfig/tsconfig.json
+      // This lets you use absolute paths in imports inside large monorepos:
+      if (process.env.NODE_PATH) {
+        console.log(
+          chalk.yellow(
+            'Setting NODE_PATH to resolve modules absolutely has been deprecated in favor of setting baseUrl in jsconfig.json (or tsconfig.json if you are using TypeScript) and will be removed in a future major release of create-react-app.'
+          )
+        );
+        console.log();
+      }
+
+      console.log(chalk.cyan('Starting the development server...\n'));
+      openBrowser(urls.localUrlForBrowser);
+    });
+
+    ['SIGINT', 'SIGTERM'].forEach(function(sig) {
+      process.on(sig, function() {
+        devServer.close();
+        process.exit();
+      });
+    });
+
+    if (isInteractive || process.env.CI !== 'true') {
+      // Gracefully exit when stdin ends
+      process.stdin.on('end', function() {
+        devServer.close();
+        process.exit();
+      });
+      process.stdin.resume();
+    }
+  })
+  .catch(err => {
+    if (err && err.message) {
+      console.log(err.message);
+    }
+    process.exit(1);
+  });

+ 53 - 0
frontend/chuchufe/scripts/test.js

@@ -0,0 +1,53 @@
+'use strict';
+
+// Do this as the first thing so that any code reading it knows the right env.
+process.env.BABEL_ENV = 'test';
+process.env.NODE_ENV = 'test';
+process.env.PUBLIC_URL = '';
+
+// Makes the script crash on unhandled rejections instead of silently
+// ignoring them. In the future, promise rejections that are not handled will
+// terminate the Node.js process with a non-zero exit code.
+process.on('unhandledRejection', err => {
+  throw err;
+});
+
+// Ensure environment variables are read.
+require('../config/env');
+
+
+const jest = require('jest');
+const execSync = require('child_process').execSync;
+let argv = process.argv.slice(2);
+
+function isInGitRepository() {
+  try {
+    execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' });
+    return true;
+  } catch (e) {
+    return false;
+  }
+}
+
+function isInMercurialRepository() {
+  try {
+    execSync('hg --cwd . root', { stdio: 'ignore' });
+    return true;
+  } catch (e) {
+    return false;
+  }
+}
+
+// Watch unless on CI or explicitly running all tests
+if (
+  !process.env.CI &&
+  argv.indexOf('--watchAll') === -1 &&
+  argv.indexOf('--watchAll=false') === -1
+) {
+  // https://github.com/facebook/create-react-app/issues/5210
+  const hasSourceControl = isInGitRepository() || isInMercurialRepository();
+  argv.push(hasSourceControl ? '--watch' : '--watchAll');
+}
+
+
+jest.run(argv);

+ 24 - 8
frontend/chuchufe/src/App.js

@@ -1,19 +1,35 @@
-import React from 'react';
+import React, { useState } from 'react';
 import './css/App.css';
-import useRoom from "./useRoom";
+import './css/fontawesome.css';
+import './css/solid.css';
+import './css/bootstrap/bootstrap.css';
+import './css/main.css';
+import useRoom from "./util/useRoom";
+import ChooseRoom from "./components/ChooseRoom";
+import Room from "./components/Room";
 
 
-const PATH = new URLSearchParams(window.location.search).get("room")
-if (PATH === "" || PATH === undefined || PATH === null) {
-    window.location = "/";
-}
+// const PATH = new URLSearchParams(window.location.search).get("room")
+// if (PATH === "" || PATH === undefined || PATH === null) {
+//     window.location = "/";
+// }
 
 
 
 function App() {
-    const [room] = useRoom();
+    const [path, setPath] = useState(null);
+    const {room, setRoom, resolver, socket} = useRoom(path);
 
-    return JSON.stringify(room);
+    if (socket.connected) {
+        return <Room room={room}
+                     setRoom={setRoom}
+                     exitRoom={() => setPath(null)}
+                     socket={socket}
+                     resolver={resolver}
+        />;
+    } else {
+        return <ChooseRoom setPath={setPath}/>
+    }
 }
 
 export default App;

+ 29 - 0
frontend/chuchufe/src/components/ChooseRoom.js

@@ -0,0 +1,29 @@
+import React from 'react';
+import logo100 from '../img/logo-100.png'
+
+
+export default function ChooseRoom({setPath}) {
+    function onSubmit(e) {
+        e.preventDefault();
+        const path = e.target[0].value
+        if (path && path !== "") {
+            setPath(path);
+        }
+    }
+
+    return <div className="container absoluteCenter" style={{height: "135px"}}>
+        <div style={{marginTop: "-100px"}}>
+            <div className="row mb-4">
+                <img className="mx-auto" alt="ChuChube logo" src={logo100} />
+            </div>
+            <div className="row">
+                <form className="mx-auto" id="roomInputForm" onSubmit={onSubmit}>
+                    <div className="form-group">
+                        <label className="sr-only" htmlFor="roomInput">Room Input</label>
+                        <input className="form-control" id="roomInput" placeholder="Room" autoFocus/>
+                    </div>
+                </form>
+            </div>
+        </div>
+    </div>
+}

+ 8 - 0
frontend/chuchufe/src/components/NavBar.js

@@ -0,0 +1,8 @@
+import React from "react";
+import logo100 from '../img/logo-100.png';
+
+export default function NavBar({ exitRoom }) {
+    return <button className="btn btn-link navBarLogo" onClick={exitRoom}>
+        <img src={logo100} alt="ChuChube Logo" height="50px"/>
+    </button>
+}

+ 82 - 0
frontend/chuchufe/src/components/Player.js

@@ -0,0 +1,82 @@
+import React, { Fragment, useEffect, useState } from "react";
+import YoutubePlayer from "./YoutubePlayer";
+import PlayerToolbar from "./PlayerToolbar";
+import { makeMessage } from "../util/Resolver";
+import { MessageTypes } from "../util/enums";
+import { Chueue } from "../util/room";
+
+
+export default function Player({ room, setRoom, socket }) {
+    const { playback, controller, chueue } = room;
+    const { isMe } = controller;
+    const { repeatEnabled } = chueue;
+
+    const [playerEnabled, setPlayerEnabled] = useState(false);
+    const [previousPlayerEnabled, setPreviousPlayerEnabled] = useState(playerEnabled);
+    const [placeholderHidden, setPlaceholderHidden] = useState(false);
+
+    useEffect(() => {
+        setPreviousPlayerEnabled(playerEnabled);
+    }, [playerEnabled]);
+
+    useEffect(() => {
+        console.log(socket);
+        if (socket.ws.readyState === WebSocket.OPEN && playerEnabled !== previousPlayerEnabled) {
+            socket.ws.send(makeMessage(MessageTypes.PLAYER_ENABLED, { enabled: playerEnabled }))
+        }
+    }, [playerEnabled, previousPlayerEnabled, socket])
+
+
+    function onSongEnd() {
+        if (isMe && playback.song !== null) {
+            socket.ws.send(makeMessage(MessageTypes.SONG_END, { id: playback.song.id }))
+            const { song: newSong, chueue: newChueue } = Chueue.pop(chueue)
+            setRoom({
+                ...room,
+                chueue: { ...chueue, ...newChueue },
+                playback: { ...playback, song: newSong },
+            })
+        }
+    }
+
+    let player;
+
+    if (playerEnabled) {
+        player = <YoutubePlayer playback={playback} onSongEnd={onSongEnd}/>
+    } else if (placeholderHidden) {
+        player =
+            <button className="btn btn-link btn-sm text-secondary pl-2" onClick={() => setPlaceholderHidden(false)}>
+                Show
+            </button>
+    } else {
+        player = <div className="col-12">
+            <div className="playerContainer">
+                <div className="d-flex justify-content-center playerPlaceholder">
+                    <div>
+                        <button className="btn btn-outline-secondary"
+                                onClick={() => setPlayerEnabled(true)}>
+                            Start Player
+                        </button>
+                        <br/>
+                        <button className="btn btn-link btn-sm small text-secondary"
+                                style={{ width: "100%" }}
+                                onClick={() => setPlaceholderHidden(true)}>
+                            Hide
+                        </button>
+                    </div>
+                </div>
+            </div>
+        </div>
+    }
+
+    return <Fragment>
+        <div className="row">
+            <PlayerToolbar playback={playback} repeatEnabled={repeatEnabled}
+                           playerEnabled={playerEnabled} setPlayerEnabled={setPlayerEnabled}
+                           socket={socket} isController={isMe}/>
+        </div>
+        <div className="row">
+            {player}
+        </div>
+    </Fragment>
+}

+ 90 - 0
frontend/chuchufe/src/components/PlayerToolbar.js

@@ -0,0 +1,90 @@
+import React, { Fragment } from "react";
+import { makeMessage } from "../util/Resolver";
+import { MediaAction, MessageTypes, PlayerState } from "../util/enums";
+
+function onButtonClass(on) {
+    if (on) {
+        return 'btn-secondary'
+    } else {
+        return 'btn-outline-secondary'
+    }
+}
+
+export default function PlayerToolbar({ playback, repeatEnabled, playerEnabled, setPlayerEnabled, isController, socket }) {
+    const { state, song } = playback;
+
+    function onControllerClick(event) {
+        event.preventDefault();
+        if (!playerEnabled || !state) {
+            return;
+        }
+        if (isController) {
+            socket.ws.send(makeMessage(MessageTypes.RELEASE_CONTROL))
+        } else {
+            socket.ws.send(makeMessage(MessageTypes.OBTAIN_CONTROL))
+        }
+    }
+
+    function onPlayClick(event) {
+        event.preventDefault();
+        if (state === PlayerState.PAUSED) {
+            socket.ws.send(makeMessage(MessageTypes.MEDIA_ACTION, { action: MediaAction.PLAY }))
+        }
+    }
+
+    function onPauseClick(event) {
+        event.preventDefault();
+        if (state === PlayerState.PLAYING) {
+            socket.ws.send(makeMessage(MessageTypes.MEDIA_ACTION, { action: MediaAction.PAUSE }))
+        }
+    }
+
+    function onNextClick(event) {
+        event.preventDefault();
+        console.log("next", song)
+        if (song !== null) {
+            socket.ws.send(makeMessage(MessageTypes.MEDIA_ACTION, { action: MediaAction.NEXT, current_id: song.id }))
+        }
+    }
+
+    function onRepeatClick(event) {
+        event.preventDefault();
+        socket.ws.send(makeMessage(MessageTypes.MEDIA_ACTION, { action: MediaAction.REPEAT, enable: !repeatEnabled }))
+    }
+
+    return <Fragment>
+        <div className="btn-toolbar col-11">
+            <div className="btn-group mr-2">
+                <button className={`btn ${onButtonClass(isController)}`}
+                        disabled={!state || !playerEnabled}
+                        onClick={onControllerClick}
+                        title={!state ? "Please check your internet connectivity or contact the owner" :
+                            !playerEnabled ? "You need to enable the player before you can be leader" :
+                                isController ? "Click to release leadership of this room" : "Click to obtain leadership of this room"}>
+                    {!state ? "No connection" :
+                        !playerEnabled ? "Remote" :
+                            isController ? "Leader" : "Listener"}
+                </button>
+            </div>
+            <div className="btn-group mr-2">
+                <button className={`btn ${onButtonClass(state === PlayerState.PLAYING)}`}
+                        onClick={onPlayClick}>
+                    <i className="fa fa-play"/></button>
+                <button className={`btn ${onButtonClass(state === PlayerState.PAUSED)}`}
+                        onClick={onPauseClick}>
+                    <i className="fa fa-pause"/></button>
+                <button className="btn btn-outline-secondary" onClick={onNextClick}>
+                    <i className="fa fa-forward"/></button>
+            </div>
+            <div className="btn-group mr-2" title="repeat">
+                <button className={`btn ${onButtonClass(repeatEnabled)}`} onClick={onRepeatClick}>
+                    <i className="fa fa-redo-alt"/></button>
+            </div>
+        </div>
+        {playerEnabled &&
+        <span className="col-1">
+            <button className="btn btn-link closePlayer" onClick={() => setPlayerEnabled(false)}><i className="fa fa-times"/></button>
+        </span>
+        }
+    </Fragment>
+}

+ 56 - 0
frontend/chuchufe/src/components/Queue.js

@@ -0,0 +1,56 @@
+import React from "react";
+import { ListOperationTypes, MessageTypes } from "../util/enums";
+import { makeMessage } from "../util/Resolver";
+
+
+export default function Queue({ room, socket }) {
+    const { chueue, videoInfoMap } = room
+    const { queue } = chueue;
+
+    function onDeleteClick(e, id) {
+        e.preventDefault();
+        socket.ws.send(makeMessage(MessageTypes.LIST_OPERATION, { op: ListOperationTypes.DEL, id }))
+    }
+
+    function onMoveClick(e, id, displacement) {
+        e.preventDefault();
+        socket.ws.send(makeMessage(MessageTypes.LIST_OPERATION, { op: ListOperationTypes.MOVE, id, displacement }))
+    }
+
+    return <div className="list-group">
+        {queue.map(({ code, id }) => {
+            // TODO PublishTime
+            const { thumbnails, title, channelTitle, description } = videoInfoMap.has(code) ? videoInfoMap.get(code) : {};
+            return <div className="videoListCard list-group-item" key={id}>
+                <div className="row">
+                    <div className="col-1">
+                        <div className="videoListCardMoveUp"
+                             onClick={(e) => onMoveClick(e, id, -1)}>
+                            <i className="fa fa-caret-up"/>
+                        </div>
+                        <div className="videoListCardMoveDown"
+                             onClick={(e) => onMoveClick(e, id, 1)}>
+                            <i className="fa fa-caret-down"/>
+                        </div>
+                    </div>
+                    <div className="videoListCardThumbnail col-3">
+                        {thumbnails && thumbnails.default &&
+                        <img src={thumbnails.default.url} width={thumbnails.default.width}
+                             height={thumbnails.default.height} alt=""/>}
+                    </div>
+                    <div className="col-7 videoListCardTextContainer">
+                        <span className="h4 videoListCardTitle">{title || "..."}</span><br/>
+                        <span className="videoListCardChannel pr-3">{channelTitle || "..."}</span>
+                        <span className="videoListCardDescription">{description || "..."}</span>
+                    </div>
+                    <div className="col-1">
+                        <button className="videoListCardDelete btn btn-outline-danger"
+                                onClick={(e) => onDeleteClick(e, id)}>
+                            <i className="fa fa-trash-alt"/>
+                        </button>
+                    </div>
+                </div>
+            </div>
+        })}
+    </div>
+}

+ 34 - 0
frontend/chuchufe/src/components/Room.js

@@ -0,0 +1,34 @@
+import React, { Fragment } from "react";
+import NavBar from "./NavBar";
+import Search from "./Search";
+import Player from "./Player";
+import Queue from "./Queue";
+
+export default function Room({ room, setRoom, exitRoom, resolver, socket }) {
+    console.log(room);
+
+    return <Fragment>
+        <NavBar exitRoom={exitRoom}/>
+        <div className="container">
+            <div className="row">
+                <div className="col-xl-6 col-sm-12">
+                    <Search socket={socket} resolver={resolver}
+                            videoInfoMap={room.videoInfoMap}
+                            setVideoInfoMap={(m) => setRoom({...room, videoInfoMap: m})}/>
+                </div>
+                <div className="col-xl-6 col-sm-12">
+                    <div className="row">
+                        <div className="col-12">
+                            <Player room={room} setRoom={setRoom} socket={socket}/>
+                        </div>
+                    </div>
+                    <div className="row">
+                        <div className="col-12">
+                            <Queue room={room} setRoom={setRoom} socket={socket} resolver={resolver}/>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </Fragment>
+}

+ 34 - 0
frontend/chuchufe/src/components/Search.js

@@ -0,0 +1,34 @@
+import React, { Fragment, useEffect, useState } from "react";
+import { MessageTypes } from "../util/enums";
+import { makeMessage } from "../util/Resolver";
+import SearchResults from "./SearchResults";
+
+export default function Search({ resolver, socket, videoInfoMap, setVideoInfoMap }) {
+    const [searchItems, setSearchItems] = useState();
+
+
+    useEffect(() => {
+        resolver.register(MessageTypes.SEARCH, (_, { items }) => setSearchItems(items))
+        return () => resolver.unregister(MessageTypes.SEARCH)
+    }, [resolver])
+
+    function search(event) {
+        event.preventDefault();
+        const q = event.target[0].value
+        if (q !== "" && socket.connected) {
+            socket.ws.send(makeMessage(MessageTypes.SEARCH, { q }))
+        }
+    }
+
+    return <Fragment>
+        <div className="row">
+            <form id="searchVideoForm" className="form-inline my-4" onSubmit={search}>
+                <div className="form-group">
+                    <label htmlFor="searchVideo" className="sr-only">Search</label>
+                    <input id="searchVideo" className="form-control ml-2" placeholder="Search Video"/>
+                </div>
+            </form>
+        </div>
+        <SearchResults searchResults={searchItems} videoInfoMap={videoInfoMap} setVideoInfoMap={setVideoInfoMap} socket={socket}/>
+    </Fragment>
+}

+ 68 - 0
frontend/chuchufe/src/components/SearchResults.js

@@ -0,0 +1,68 @@
+import React from 'react';
+import { ListOperationTypes, MessageTypes, YoutubeResourceType } from "../util/enums";
+import { makeMessage } from "../util/Resolver";
+import useMapBuilder from "../util/useMapBuilder";
+
+function makeSearchResult(item, socket, addVideoInfo) {
+    const { id, snippet } = item
+    const { kind, videoId, channelId, playlistId } = id
+    const { thumbnails, title, channelTitle, description } = snippet
+
+    const code = kind === YoutubeResourceType.CHANNEL ? channelId :
+        kind === YoutubeResourceType.PLAYLIST ? playlistId :
+            kind === YoutubeResourceType.VIDEO ? videoId :
+                console.error(`Unknown kind ${kind}`)
+
+    if (!code) return null;
+
+    function onClickHandler() {
+        socket.ws.send(makeMessage(MessageTypes.LIST_OPERATION, {
+            op: ListOperationTypes.ADD,
+            kind,
+            code
+        }));
+        if (kind === YoutubeResourceType.VIDEO) {
+            addVideoInfo(videoId, snippet)
+        }
+    }
+
+    const { url, width, height } = (thumbnails ? thumbnails["default"] : {
+        url: "/img/no_thumbnail.png",
+        width: 120,
+        height: 90
+    })
+
+    return <div className="searchResult list-group-item list-group-item-action" onClick={onClickHandler} key={code}>
+        <div className="row">
+            <div className="searchResultThumbnail col-3">
+                <div className="thumbnailContainer">
+                    <div className="thumbnailImage">
+                        <img src={url} width={width} height={height} alt=""/>
+                    </div>
+                    {kind === YoutubeResourceType.PLAYLIST &&
+                    <div className="thumbnailPlaylistOverlay">
+                        <i className="fa fa-bars"/>
+                    </div>
+                    }
+                </div>
+            </div>
+            <div className="col-9 searchResultTextContainer">
+                <span className="h4 searchResultTitle">{title}</span><br/>
+                <span className="searchResultChannel pr-3">{channelTitle}</span>
+                <span className="searchResultDescription">{description}</span>
+            </div>
+        </div>
+    </div>
+
+}
+
+export default function SearchResults({ searchResults, socket, videoInfoMap, setVideoInfoMap }) {
+
+    const addVideoInfo = (k, v) => setVideoInfoMap(new Map(videoInfoMap).set(k,v));
+
+    return <div className="row">
+        <div className="list-group col">
+            {(searchResults || []).map((item) => makeSearchResult(item, socket, addVideoInfo))}
+        </div>
+    </div>
+}

+ 137 - 0
frontend/chuchufe/src/components/YoutubePlayer.js

@@ -0,0 +1,137 @@
+import React, { useEffect, useMemo, useRef, useState } from "react";
+import { PlayerState, YoutubePlayerState } from "../util/enums";
+
+const PLAYER_WIDTH = 640
+const PLAYER_HEIGHT = 360
+
+export default function YoutubePlayer({ playback, onSongEnd }) {
+    const { song, state } = playback;
+    const id = useMemo(() => `youtube-player-${Math.floor(1e15 * Math.random())}`, []);
+    const player = useRef(null)
+    const [iframeAPILoading, setIframeAPILoading] = useState(false);
+    const [playerConnected, setPlayerConnected] = useState(false)
+    const [previousState, setPreviousState] = useState(state)
+    const [iframeSong, setIframeSong] = useState(null);
+
+
+    useEffect(() => {
+        console.log({ playerConnected, song, iframeSong, state, previousState })
+        if (playerConnected) {
+            if (song !== null) {
+                if (previousState !== state && iframeSong !== null) {
+                    switch (state) {
+                        case PlayerState.PLAYING:
+                            if (previousState !== PlayerState.PLAYING) {
+                                player.current.playVideo();
+                            }
+                            break;
+                        case PlayerState.PAUSED:
+                            if (previousState !== PlayerState.PAUSED) {
+                                player.current.pauseVideo();
+                            }
+                            break;
+                        case PlayerState.LIST_END:
+                            // Soon to be deprecated
+                            if (previousState !== PlayerState.LIST_END) {
+                                player.current.stopVideo();
+                            }
+                            break;
+                        default:
+                            console.warn("Uknown PlayerState: ", state)
+                    }
+                    setPreviousState(state);
+                }
+                if (iframeSong === null || iframeSong.id !== song.id) {
+                    switch (state) {
+                        case PlayerState.PLAYING:
+                            player.current.loadVideoById(song.code);
+                            setIframeSong(song);
+                            break;
+                        case PlayerState.PAUSED:
+                            player.current.cueVideoById(song.code);
+                            setIframeSong(song);
+                            break
+                        case PlayerState.LIST_END:
+                            player.current.stopVideo();
+                            console.warn("Anomalous state reached:", {
+                                song,
+                                iframeSong,
+                                state,
+                                iframeState: previousState
+                            })
+                            setIframeSong(null);
+                            break;
+                        default:
+                            console.warn("Uknown PlayerState: ", state)
+                    }
+                }
+
+            } else {
+                if (iframeSong !== null) {
+                    player.current.stopVideo();
+                    setIframeSong(null);
+                    setPreviousState(state);
+                }
+            }
+        }
+    }, [state, previousState, song, iframeSong, playerConnected])
+
+    useEffect(() => {
+        if (!playerConnected) {
+            function onYTIAPIReady() {
+                player.current = new window.YT.Player(id, {
+                    // height: PLAYER_HEIGHT + 48,
+                    // width: PLAYER_WIDTH,
+                    videoId: undefined,
+                    playerVars: {
+                        "origin": window.location.origin,
+                        "autoplay": 1
+                    },
+                    events: {
+                        'onReady': () => {
+                            setPlayerConnected(true);
+                            setIframeAPILoading(false);
+                        }
+                    }
+                })
+                console.log("Player preparing");
+            }
+
+            window.onYouTubeIframeAPIReady = onYTIAPIReady;
+
+            if (!iframeAPILoading) {
+                if (!window.YT) {
+                    setIframeAPILoading(true);
+                    const tag = document.createElement('script');
+                    tag.src = 'https://www.youtube.com/iframe_api';
+                    const firstScriptTag = document.getElementsByTagName('script')[0];
+                    firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
+                } else {
+                    window.onYouTubeIframeAPIReady()
+                }
+            }
+        }
+    }, [id, iframeAPILoading, playerConnected]);
+
+    useEffect(() => {
+        if (playerConnected) {
+            function onPlayerStateChange(event) {
+                if (event.data === YoutubePlayerState.ENDED) {
+                    onSongEnd();
+                }
+            }
+
+            const currentPlayer = player.current
+            currentPlayer.addEventListener("onStateChange", onPlayerStateChange)
+            return () => currentPlayer.removeEventListener("onStateChange", onPlayerStateChange)
+        }
+    }, [onSongEnd, playerConnected])
+
+    return <div className="col-12">
+        <div className="playerContainer" hidden={!playerConnected || song === null}>
+            <div id={id}/>
+        </div>
+        <div className="playerContainer" hidden={playerConnected && song !== null}
+             style={{ background: "white" }}/>
+    </div>
+}

+ 0 - 0
frontend/chuchufe/public/css/bootstrap/bootstrap.css → frontend/chuchufe/src/css/bootstrap/bootstrap.css


+ 0 - 0
frontend/chuchufe/public/css/bootstrap/bootstrap.css.map → frontend/chuchufe/src/css/bootstrap/bootstrap.css.map


+ 0 - 0
frontend/chuchufe/public/css/bootstrap/bootstrap.min.css → frontend/chuchufe/src/css/bootstrap/bootstrap.min.css


+ 0 - 0
frontend/chuchufe/public/css/bootstrap/bootstrap.min.css.map → frontend/chuchufe/src/css/bootstrap/bootstrap.min.css.map


+ 0 - 0
frontend/chuchufe/public/css/fontawesome.css → frontend/chuchufe/src/css/fontawesome.css


+ 0 - 0
frontend/chuchufe/public/css/fontawesome.min.css → frontend/chuchufe/src/css/fontawesome.min.css


+ 27 - 7
frontend/chuchufe/public/css/main.css → frontend/chuchufe/src/css/main.css

@@ -1,6 +1,6 @@
 @font-face {
     font-family: "Kenyan Coffee";
-    src: url("../webfonts/kenyc.ttf");
+    src: url("./../webfonts/kenyc.ttf");
 }
 
 @media (min-width: 1500px) {
@@ -21,23 +21,43 @@ input, select, textarea, button {
     font-family: inherit;
 }
 
-#logo {
+.navBarLogo {
     position: absolute;
     top: 20px;
     left: 20px
 }
 
-#playerPlaceholder {
-    width: 640px;
-    height: 422px;
+.container {
+    margin-bottom: 20px;
+}
+
+.playerPlaceholder {
     border: dashed lightgray;
     border-radius: 5%;
-    padding-top: 192px;
+    padding-top: 22%;
+}
+
+.playerContainer {
+    margin-top: 20px;
+    margin-bottom: 20px;
+    position: relative;
+    width: 100%;
+    height: 0;
+    padding-bottom: 56.25%;
+}
+
+.playerContainer iframe, .playerContainer .playerPlaceholder {
+    position: absolute;
+    top: 0;
+    /*left: 0;*/
+    width: 100%;
+    height: 100%
 }
 
-#closePlayer {
+.closePlayer {
     font-size: 18pt;
     color: black;
+    padding: 0 12px;
 }
 
 .searchResultChannel, .videoListCardChannel {

+ 0 - 0
frontend/chuchufe/public/css/normalize.css → frontend/chuchufe/src/css/normalize.css


+ 0 - 0
frontend/chuchufe/src/css/index.css → frontend/chuchufe/src/css/player.css


+ 0 - 0
frontend/chuchufe/public/css/regular.css → frontend/chuchufe/src/css/regular.css


+ 0 - 0
frontend/chuchufe/public/css/regular.min.css → frontend/chuchufe/src/css/regular.min.css


+ 0 - 0
frontend/chuchufe/public/css/solid.css → frontend/chuchufe/src/css/solid.css


+ 0 - 0
frontend/chuchufe/public/css/solid.min.css → frontend/chuchufe/src/css/solid.min.css


BIN
frontend/chuchufe/src/img/favicon.png


BIN
frontend/chuchufe/src/img/icon.png


+ 633 - 0
frontend/chuchufe/src/img/icon.svg

@@ -0,0 +1,633 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   viewBox="0 0 156 156"
+   version="1.1"
+   id="svg887"
+   sodipodi:docname="icon.svg"
+   inkscape:version="0.92.3 (2405546, 2018-03-11)"
+   width="156"
+   height="156">
+  <metadata
+     id="metadata893">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs891">
+    <inkscape:perspective
+       id="perspective5329"
+       inkscape:persp3d-origin="0.75619357 : 3.1889763 : 1"
+       inkscape:vp_z="1.5123871 : 4.7834644 : 1"
+       inkscape:vp_y="0 : 9.090909 : 0"
+       inkscape:vp_x="0 : 4.7834644 : 1"
+       sodipodi:type="inkscape:persp3d" />
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.0045454545 : 1"
+       inkscape:vp_y="0 : 9.090909 : 0"
+       inkscape:vp_z="0.0020325203 : 0.0045454545 : 1"
+       inkscape:persp3d-origin="0.0010162602 : 0.003030303 : 1"
+       id="perspective5339" />
+    <clipPath
+       id="clipPath4202"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path4204"
+         d="m 20.045,731.714 h 42.114 v 37.012 H 20.045 Z"
+         inkscape:connector-curvature="0" />
+    </clipPath>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 1.9090909 : 1"
+       inkscape:vp_y="0 : 9.090909 : 0"
+       inkscape:vp_z="1.7276423 : 1.9090909 : 1"
+       inkscape:persp3d-origin="0.86382113 : 1.2727273 : 1"
+       id="perspective5396" />
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.0045454545 : 1"
+       inkscape:vp_y="0 : 9.090909 : 0"
+       inkscape:vp_z="0.0020325203 : 0.0045454545 : 1"
+       inkscape:persp3d-origin="0.0010162602 : 0.003030303 : 1"
+       id="perspective5429" />
+    <clipPath
+       id="clipPath2840"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path2842"
+         d="m 77.057,731.714 h 42.114 v 37.012 H 77.057 Z"
+         inkscape:connector-curvature="0" />
+    </clipPath>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.0045454545 : 1"
+       inkscape:vp_y="0 : 9.090909 : 0"
+       inkscape:vp_z="0.0020325203 : 0.0045454545 : 1"
+       inkscape:persp3d-origin="0.0010162602 : 0.003030303 : 1"
+       id="perspective5499" />
+    <clipPath
+       id="clipPath3036"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path3038"
+         d="m 134.072,731.751 h 42.114 v 36.975 h -42.114 z"
+         inkscape:connector-curvature="0" />
+    </clipPath>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.0045454545 : 1"
+       inkscape:vp_y="0 : 9.090909 : 0"
+       inkscape:vp_z="0.0020325203 : 0.0045454545 : 1"
+       inkscape:persp3d-origin="0.0010162602 : 0.003030303 : 1"
+       id="perspective5621" />
+    <clipPath
+       id="clipPath2952"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path2954"
+         d="m 191.084,731.751 h 42.113 v 36.975 h -42.113 z"
+         inkscape:connector-curvature="0" />
+    </clipPath>
+    <clipPath
+       id="clipPath2936"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path2938"
+         d="m 191.084,731.751 h 42.113 v 36.975 h -42.113 z"
+         inkscape:connector-curvature="0" />
+    </clipPath>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.0045454545 : 1"
+       inkscape:vp_y="0 : 9.090909 : 0"
+       inkscape:vp_z="0.0020325203 : 0.0045454545 : 1"
+       inkscape:persp3d-origin="0.0010162602 : 0.003030303 : 1"
+       id="perspective5694" />
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       id="perspective18"
+       inkscape:persp3d-origin="0.51829268 : 1.3727273 : 1"
+       inkscape:vp_z="1.0365854 : 2.0590909 : 1"
+       inkscape:vp_y="0 : 9.090909 : 0"
+       inkscape:vp_x="0 : 2.0590909 : 1" />
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.0045454545 : 1"
+       inkscape:vp_y="0 : 9.090909 : 0"
+       inkscape:vp_z="0.0020325203 : 0.0045454545 : 1"
+       inkscape:persp3d-origin="0.0010162602 : 0.003030303 : 1"
+       id="perspective5722" />
+    <clipPath
+       id="clipPath3216"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path3218"
+         d="m 248.098,731.74 h 42.114 v 36.986 h -42.114 z"
+         inkscape:connector-curvature="0" />
+    </clipPath>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.0045454545 : 1"
+       inkscape:vp_y="0 : 9.090909 : 0"
+       inkscape:vp_z="0.0020325203 : 0.0045454545 : 1"
+       inkscape:persp3d-origin="0.0010162602 : 0.003030303 : 1"
+       id="perspective5860" />
+    <clipPath
+       id="clipPath2880"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path2882"
+         d="m 305.111,731.751 h 42.114 v 36.975 h -42.114 z"
+         inkscape:connector-curvature="0" />
+    </clipPath>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.0045454545 : 1"
+       inkscape:vp_y="0 : 9.090909 : 0"
+       inkscape:vp_z="0.0020325203 : 0.0045454545 : 1"
+       inkscape:persp3d-origin="0.0010162602 : 0.003030303 : 1"
+       id="perspective5942" />
+    <clipPath
+       id="clipPath3112"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path3114"
+         d="m 362.126,731.751 h 42.113 v 36.975 h -42.113 z"
+         inkscape:connector-curvature="0" />
+    </clipPath>
+    <clipPath
+       id="clipPath3100"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path3102"
+         d="m 362.126,731.751 h 42.114 v 36.975 h -42.114 z"
+         inkscape:connector-curvature="0" />
+    </clipPath>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.0045454545 : 1"
+       inkscape:vp_y="0 : 9.090909 : 0"
+       inkscape:vp_z="0.0020325203 : 0.0045454545 : 1"
+       inkscape:persp3d-origin="0.0010162602 : 0.003030303 : 1"
+       id="perspective6017" />
+    <clipPath
+       id="clipPath2984"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path2986"
+         d="m 419.137,731.751 h 42.114 v 36.975 h -42.114 z"
+         inkscape:connector-curvature="0" />
+    </clipPath>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.0045454545 : 1"
+       inkscape:vp_y="0 : 9.090909 : 0"
+       inkscape:vp_z="0.0020325203 : 0.0045454545 : 1"
+       inkscape:persp3d-origin="0.0010162602 : 0.003030303 : 1"
+       id="perspective6119" />
+    <clipPath
+       id="clipPath3296"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path3298"
+         d="m 476.153,731.751 h 42.113 v 36.975 h -42.113 z"
+         inkscape:connector-curvature="0" />
+    </clipPath>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.0045454545 : 1"
+       inkscape:vp_y="0 : 9.090909 : 0"
+       inkscape:vp_z="0.0020325203 : 0.0045454545 : 1"
+       inkscape:persp3d-origin="0.0010162602 : 0.003030303 : 1"
+       id="perspective6219" />
+    <clipPath
+       id="clipPath2860"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path2862"
+         d="m 533.165,731.751 h 42.113 v 36.975 h -42.113 z"
+         inkscape:connector-curvature="0" />
+    </clipPath>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.0045454545 : 1"
+       inkscape:vp_y="0 : 9.090909 : 0"
+       inkscape:vp_z="0.0020325203 : 0.0045454545 : 1"
+       inkscape:persp3d-origin="0.0010162602 : 0.003030303 : 1"
+       id="perspective6281" />
+    <clipPath
+       id="clipPath3080"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path3082"
+         d="m 20.045,674.69 h 42.114 v 36.975 H 20.045 Z"
+         inkscape:connector-curvature="0" />
+    </clipPath>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.0045454545 : 1"
+       inkscape:vp_y="0 : 9.090909 : 0"
+       inkscape:vp_z="0.0020325203 : 0.0045454545 : 1"
+       inkscape:persp3d-origin="0.0010162602 : 0.003030303 : 1"
+       id="perspective6343" />
+    <clipPath
+       id="clipPath2964"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path2966"
+         d="m 77.057,674.69 h 42.114 v 36.975 H 77.057 Z"
+         inkscape:connector-curvature="0" />
+    </clipPath>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.0045454545 : 1"
+       inkscape:vp_y="0 : 9.090909 : 0"
+       inkscape:vp_z="0.0020325203 : 0.0045454545 : 1"
+       inkscape:persp3d-origin="0.0010162602 : 0.003030303 : 1"
+       id="perspective6432" />
+    <clipPath
+       id="clipPath3256"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path3258"
+         d="m 134.072,674.69 h 42.114 v 36.975 h -42.114 z"
+         inkscape:connector-curvature="0" />
+    </clipPath>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.0045454545 : 1"
+       inkscape:vp_y="0 : 9.090909 : 0"
+       inkscape:vp_z="0.0020325203 : 0.0045454545 : 1"
+       inkscape:persp3d-origin="0.0010162602 : 0.003030303 : 1"
+       id="perspective6560" />
+    <clipPath
+       id="clipPath2908"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path2910"
+         d="m 191.084,674.69 h 42.113 v 36.975 h -42.113 z"
+         inkscape:connector-curvature="0" />
+    </clipPath>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.0045454545 : 1"
+       inkscape:vp_y="0 : 9.090909 : 0"
+       inkscape:vp_z="0.0020325203 : 0.0045454545 : 1"
+       inkscape:persp3d-origin="0.0010162602 : 0.003030303 : 1"
+       id="perspective6650" />
+    <clipPath
+       id="clipPath3128"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path3130"
+         d="m 248.098,674.69 h 42.114 v 36.975 h -42.114 z"
+         inkscape:connector-curvature="0" />
+    </clipPath>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.0045454545 : 1"
+       inkscape:vp_y="0 : 9.090909 : 0"
+       inkscape:vp_z="0.0020325203 : 0.0045454545 : 1"
+       inkscape:persp3d-origin="0.0010162602 : 0.003030303 : 1"
+       id="perspective6920" />
+    <clipPath
+       id="clipPath3016"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path3018"
+         d="m 305.111,674.69 h 42.114 v 36.975 h -42.114 z"
+         inkscape:connector-curvature="0" />
+    </clipPath>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.0045454545 : 1"
+       inkscape:vp_y="0 : 9.090909 : 0"
+       inkscape:vp_z="0.0020325203 : 0.0045454545 : 1"
+       inkscape:persp3d-origin="0.0010162602 : 0.003030303 : 1"
+       id="perspective6991" />
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.0045454545 : 1"
+       inkscape:vp_y="0 : 9.090909 : 0"
+       inkscape:vp_z="0.0020325203 : 0.0045454545 : 1"
+       inkscape:persp3d-origin="0.0010162602 : 0.003030303 : 1"
+       id="perspective7016" />
+    <clipPath
+       id="clipPath4234"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path4236"
+         d="m 362.126,674.673 h 42.113 v 36.975 h -42.113 z"
+         inkscape:connector-curvature="0" />
+    </clipPath>
+    <clipPath
+       id="clipPath4222"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path4224"
+         d="m 362.126,674.673 h 42.114 v 36.975 h -42.114 z"
+         inkscape:connector-curvature="0" />
+    </clipPath>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.0045454545 : 1"
+       inkscape:vp_y="0 : 9.090909 : 0"
+       inkscape:vp_z="0.0020325203 : 0.0045454545 : 1"
+       inkscape:persp3d-origin="0.0010162602 : 0.003030303 : 1"
+       id="perspective7091" />
+    <clipPath
+       id="clipPath4394"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path4396"
+         d="m 419.137,674.673 h 42.114 v 36.975 h -42.114 z"
+         inkscape:connector-curvature="0" />
+    </clipPath>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.0045454545 : 1"
+       inkscape:vp_y="0 : 9.090909 : 0"
+       inkscape:vp_z="0.0020325203 : 0.0045454545 : 1"
+       inkscape:persp3d-origin="0.0010162602 : 0.003030303 : 1"
+       id="perspective7169" />
+    <clipPath
+       id="clipPath4848"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path4850"
+         d="m 476.152,674.673 h 42.114 v 36.975 h -42.114 z"
+         inkscape:connector-curvature="0" />
+    </clipPath>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.0045454545 : 1"
+       inkscape:vp_y="0 : 9.090909 : 0"
+       inkscape:vp_z="0.0020325203 : 0.0045454545 : 1"
+       inkscape:persp3d-origin="0.0010162602 : 0.003030303 : 1"
+       id="perspective7231" />
+    <clipPath
+       id="clipPath4976"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path4978"
+         d="m 20.044,617.671 h 42.114 v 36.975 H 20.044 Z"
+         inkscape:connector-curvature="0" />
+    </clipPath>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.0045454545 : 1"
+       inkscape:vp_y="0 : 9.090909 : 0"
+       inkscape:vp_z="0.0020325203 : 0.0045454545 : 1"
+       inkscape:persp3d-origin="0.0010162602 : 0.003030303 : 1"
+       id="perspective7293" />
+    <clipPath
+       id="clipPath4924"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path4926"
+         d="m 134.07,617.671 h 42.114 v 36.975 H 134.07 Z"
+         inkscape:connector-curvature="0" />
+    </clipPath>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.0045454545 : 1"
+       inkscape:vp_y="0 : 9.090909 : 0"
+       inkscape:vp_z="0.0020325203 : 0.0045454545 : 1"
+       inkscape:persp3d-origin="0.0010162602 : 0.003030303 : 1"
+       id="perspective7355" />
+    <clipPath
+       id="clipPath4732"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path4734"
+         d="m 191.083,617.671 h 42.114 v 36.975 h -42.114 z"
+         inkscape:connector-curvature="0" />
+    </clipPath>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.0045454545 : 1"
+       inkscape:vp_y="0 : 9.090909 : 0"
+       inkscape:vp_z="0.0020325203 : 0.0045454545 : 1"
+       inkscape:persp3d-origin="0.0010162602 : 0.003030303 : 1"
+       id="perspective7417" />
+    <clipPath
+       id="clipPath4538"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path4540"
+         d="m 305.109,617.68 h 42.114 v 36.966 h -42.114 z"
+         inkscape:connector-curvature="0" />
+    </clipPath>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.0045454545 : 1"
+       inkscape:vp_y="0 : 9.090909 : 0"
+       inkscape:vp_z="0.0020325203 : 0.0045454545 : 1"
+       inkscape:persp3d-origin="0.0010162602 : 0.003030303 : 1"
+       id="perspective7567" />
+    <clipPath
+       id="clipPath4888"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path4890"
+         d="m 362.141,617.646 h 42.114 v 36.974 h -42.114 z"
+         inkscape:connector-curvature="0" />
+    </clipPath>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.0045454545 : 1"
+       inkscape:vp_y="0 : 9.090909 : 0"
+       inkscape:vp_z="0.0020325203 : 0.0045454545 : 1"
+       inkscape:persp3d-origin="0.0010162602 : 0.003030303 : 1"
+       id="perspective7715" />
+    <clipPath
+       id="clipPath4704"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path4706"
+         d="m 419.137,617.646 h 42.114 v 36.974 h -42.114 z"
+         inkscape:connector-curvature="0" />
+    </clipPath>
+    <clipPath
+       id="clipPath4684"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path4686"
+         d="m 419.137,617.646 h 42.114 v 36.974 h -42.114 z"
+         inkscape:connector-curvature="0" />
+    </clipPath>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.0045454545 : 1"
+       inkscape:vp_y="0 : 9.090909 : 0"
+       inkscape:vp_z="0.0020325203 : 0.0045454545 : 1"
+       inkscape:persp3d-origin="0.0010162602 : 0.003030303 : 1"
+       id="perspective7822" />
+    <clipPath
+       id="clipPath5076"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path5078"
+         d="m 476.152,617.644 h 42.135 v 37.002 h -42.135 z"
+         inkscape:connector-curvature="0" />
+    </clipPath>
+    <clipPath
+       id="clipPath5064"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path5066"
+         d="m 476.152,617.645 h 42.135 v 37.001 h -42.135 z"
+         inkscape:connector-curvature="0" />
+    </clipPath>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.0045454545 : 1"
+       inkscape:vp_y="0 : 9.090909 : 0"
+       inkscape:vp_z="0.0020325203 : 0.0045454545 : 1"
+       inkscape:persp3d-origin="0.0010162602 : 0.003030303 : 1"
+       id="perspective7961" />
+    <clipPath
+       id="clipPath5016"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path5018"
+         d="m 533.182,617.715 h 42.114 v 36.931 h -42.114 z"
+         inkscape:connector-curvature="0" />
+    </clipPath>
+  </defs>
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="1920"
+     inkscape:window-height="1029"
+     id="namedview889"
+     showgrid="false"
+     inkscape:snap-object-midpoints="true"
+     inkscape:zoom="1.4146341"
+     inkscape:cx="-188.42839"
+     inkscape:cy="68.703091"
+     inkscape:window-x="0"
+     inkscape:window-y="25"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg887"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0" />
+  <flowRoot
+     style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:16px;line-height:125%;font-family:'Open Sans';-inkscape-font-specification:'Open Sans';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+     id="flowRoot895"
+     xml:space="preserve"
+     transform="translate(-0.08781227,46.533031)"><flowRegion
+       id="flowRegion897"><rect
+         y="52.517094"
+         x="264.42136"
+         height="49.182903"
+         width="35.07864"
+         id="rect899" /></flowRegion><flowPara
+       id="flowPara901" /></flowRoot>  <path
+     inkscape:connector-curvature="0"
+     style="fill:#ff0000"
+     d="m 152.48326,39.900011 a 19.6,19.6 0 0 0 -13.8,-13.8 c -12.1,-3.3 -60.800012,-3.3 -60.800012,-3.3 0,0 -48.7,0.1 -60.79999,3.4 a 19.6,19.6 0 0 0 -13.8000003,13.8 c -3.65999997,21.5 -5.08,54.259989 0.1,74.899989 a 19.6,19.6 0 0 0 13.8000003,13.8 c 12.09999,3.3 60.79999,3.3 60.79999,3.3 0,0 48.700012,0 60.800012,-3.3 a 19.6,19.6 0 0 0 13.8,-13.8 c 3.86,-21.53 5.05,-54.269989 -0.1,-74.999989 z"
+     id="path874-3" />
+  <g
+     inkscape:label="Ebene 1"
+     inkscape:groupmode="layer"
+     transform="translate(-895.63472,2.0776107)"
+     id="layer1-0" />
+  <g
+     style="fill:#ffffff"
+     id="g8075-7"
+     transform="matrix(-0.52072048,0,0,0.52072048,81.169918,-134.23916)">
+    <g
+       style="fill:#ffffff"
+       transform="matrix(9.6802871,0,0,-9.6802871,76.943493,438.0967)"
+       id="g5024-5">
+      <path
+         inkscape:connector-curvature="0"
+         id="path5026-3"
+         style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none"
+         d="m 0,0 c 0,-0.105 0.035,-0.245 0.035,-0.35 0,-1.154 -0.944,-2.064 -2.065,-2.064 -1.119,0 -2.064,0.91 -2.064,2.064 0,0.105 0.036,0.245 0.036,0.35 z" />
+    </g>
+    <g
+       style="fill:#ffffff"
+       transform="matrix(9.6802871,0,0,-9.6802871,76.943493,438.0967)"
+       id="g5028-5">
+      <path
+         inkscape:connector-curvature="0"
+         id="path5030-6"
+         style="fill:#ffffff;stroke:#000000;stroke-width:0.01431872;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2.61299992;stroke-dasharray:none;stroke-opacity:1"
+         d="m 0,0 c 0,-0.105 0.035,-0.245 0.035,-0.35 0,-1.154 -0.944,-2.064 -2.065,-2.064 -1.119,0 -2.064,0.91 -2.064,2.064 0,0.105 0.036,0.245 0.036,0.35 z" />
+    </g>
+    <g
+       style="fill:#ffffff"
+       transform="matrix(9.6802871,0,0,-9.6802871,25.803503,438.0967)"
+       id="g5032-2">
+      <path
+         inkscape:connector-curvature="0"
+         id="path5034-9"
+         style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none"
+         d="m 0,0 c 0,-0.105 0.035,-0.245 0.035,-0.35 0,-1.154 -0.944,-2.064 -2.065,-2.064 -1.119,0 -2.064,0.91 -2.064,2.064 0,0.105 0.036,0.245 0.036,0.35 z" />
+    </g>
+    <g
+       style="fill:#ffffff"
+       transform="matrix(9.6802871,0,0,-9.6802871,25.803503,438.0967)"
+       id="g5036-1">
+      <path
+         inkscape:connector-curvature="0"
+         id="path5038-2"
+         style="fill:#ffffff;stroke:#000000;stroke-width:0.01431872;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2.61299992;stroke-dasharray:none;stroke-opacity:1"
+         d="m 0,0 c 0,-0.105 0.035,-0.245 0.035,-0.35 0,-1.154 -0.944,-2.064 -2.065,-2.064 -1.119,0 -2.064,0.91 -2.064,2.064 0,0.105 0.036,0.245 0.036,0.35 z" />
+    </g>
+    <g
+       style="fill:#ffffff"
+       transform="matrix(9.6802871,0,0,-9.6802871,-25.338427,438.0967)"
+       id="g5040-7">
+      <path
+         inkscape:connector-curvature="0"
+         id="path5042-0"
+         style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none"
+         d="m 0,0 c 0.035,-0.105 0.035,-0.245 0.035,-0.35 0,-1.154 -0.91,-2.064 -2.065,-2.064 -1.119,0 -2.064,0.91 -2.064,2.064 0,0.105 0.036,0.245 0.036,0.35 z" />
+    </g>
+    <g
+       style="fill:#ffffff"
+       transform="matrix(9.6802871,0,0,-9.6802871,-25.338427,438.0967)"
+       id="g5044-9">
+      <path
+         inkscape:connector-curvature="0"
+         id="path5046-3"
+         style="fill:#ffffff;stroke:#000000;stroke-width:0.01431872;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2.61299992;stroke-dasharray:none;stroke-opacity:1"
+         d="m 0,0 c 0.035,-0.105 0.035,-0.245 0.035,-0.35 0,-1.154 -0.91,-2.064 -2.065,-2.064 -1.119,0 -2.064,0.91 -2.064,2.064 0,0.105 0.036,0.245 0.036,0.35 z" />
+    </g>
+    <g
+       style="fill:#ffffff"
+       transform="matrix(9.6802871,0,0,-9.6802871,6.160263,373.06937)"
+       id="g5048-6">
+      <path
+         inkscape:connector-curvature="0"
+         id="path5050-0"
+         style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none"
+         d="M 0,0 C 0,0.77 -0.56,1.399 -1.329,1.399 -2.1,1.399 -2.659,0.77 -2.659,0 v -0.7 c 0,-0.139 -0.14,-0.279 -0.28,-0.279 h -2.064 c -0.14,0 -0.28,0.14 -0.28,0.279 v 1.855 c 0,0.104 -0.07,0.21 -0.21,0.21 h -1.294 c -0.14,0 -0.21,-0.106 -0.21,-0.21 V -0.7 c 0,-0.139 -0.14,-0.279 -0.28,-0.279 h -0.56 c -0.175,0 -0.315,-0.14 -0.315,-0.28 v -4.653 c 0,-0.176 0.14,-0.316 0.315,-0.316 H 7.802 c 0.175,0 0.315,0.14 0.315,0.316 v 3.323 c 0,0.175 -0.14,0.315 -0.315,0.315 H 5.563 c -0.14,0 -0.28,0.104 -0.28,0.28 V 0.28 c 0,0.175 0.14,0.28 0.28,0.28 h 2.169 c 0.245,0 0.42,0.175 0.42,0.385 0,0.21 -0.175,0.42 -0.42,0.42 H 2.939 C 2.763,1.365 2.624,1.225 2.624,1.049 V -0.7 c 0,-0.139 -0.14,-0.279 -0.28,-0.279 H 0.28 C 0.14,-0.979 0,-0.839 0,-0.7 Z" />
+    </g>
+  </g>
+</svg>

BIN
frontend/chuchufe/src/img/logo-100.png


BIN
frontend/chuchufe/src/img/logo.png


+ 614 - 0
frontend/chuchufe/src/img/logo.svg

@@ -0,0 +1,614 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   viewBox="0 0 492 110"
+   version="1.1"
+   id="svg887"
+   sodipodi:docname="logo.svg"
+   inkscape:version="0.92.3 (2405546, 2018-03-11)">
+  <metadata
+     id="metadata893">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs891">
+    <inkscape:perspective
+       id="perspective5329"
+       inkscape:persp3d-origin="372.04724 : 350.78739 : 1"
+       inkscape:vp_z="744.09448 : 526.18109 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_x="0 : 526.18109 : 1"
+       sodipodi:type="inkscape:persp3d" />
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="1 : 0.5 : 1"
+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+       id="perspective5339" />
+    <clipPath
+       id="clipPath4202"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path4204"
+         d="m 20.045,731.714 42.114,0 0,37.012 -42.114,0 0,-37.012 z" />
+    </clipPath>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 210 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="850 : 210 : 1"
+       inkscape:persp3d-origin="425 : 140 : 1"
+       id="perspective5396" />
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="1 : 0.5 : 1"
+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+       id="perspective5429" />
+    <clipPath
+       id="clipPath2840"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path2842"
+         d="m 77.057,731.714 42.114,0 0,37.012 -42.114,0 0,-37.012 z" />
+    </clipPath>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="1 : 0.5 : 1"
+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+       id="perspective5499" />
+    <clipPath
+       id="clipPath3036"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path3038"
+         d="m 134.072,731.751 42.114,0 0,36.975 -42.114,0 0,-36.975 z" />
+    </clipPath>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="1 : 0.5 : 1"
+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+       id="perspective5621" />
+    <clipPath
+       id="clipPath2952"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path2954"
+         d="m 191.084,731.751 42.113,0 0,36.975 -42.113,0 0,-36.975 z" />
+    </clipPath>
+    <clipPath
+       id="clipPath2936"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path2938"
+         d="m 191.084,731.751 42.113,0 0,36.975 -42.113,0 0,-36.975 z" />
+    </clipPath>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="1 : 0.5 : 1"
+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+       id="perspective5694" />
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       id="perspective18"
+       inkscape:persp3d-origin="255 : 151 : 1"
+       inkscape:vp_z="510 : 226.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_x="0 : 226.5 : 1" />
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="1 : 0.5 : 1"
+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+       id="perspective5722" />
+    <clipPath
+       id="clipPath3216"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path3218"
+         d="m 248.098,731.74 42.114,0 0,36.986 -42.114,0 0,-36.986 z" />
+    </clipPath>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="1 : 0.5 : 1"
+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+       id="perspective5860" />
+    <clipPath
+       id="clipPath2880"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path2882"
+         d="m 305.111,731.751 42.114,0 0,36.975 -42.114,0 0,-36.975 z" />
+    </clipPath>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="1 : 0.5 : 1"
+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+       id="perspective5942" />
+    <clipPath
+       id="clipPath3112"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path3114"
+         d="m 362.126,731.751 42.113,0 0,36.975 -42.113,0 0,-36.975 z" />
+    </clipPath>
+    <clipPath
+       id="clipPath3100"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path3102"
+         d="m 362.126,731.751 42.114,0 0,36.975 -42.114,0 0,-36.975 z" />
+    </clipPath>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="1 : 0.5 : 1"
+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+       id="perspective6017" />
+    <clipPath
+       id="clipPath2984"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path2986"
+         d="m 419.137,731.751 42.114,0 0,36.975 -42.114,0 0,-36.975 z" />
+    </clipPath>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="1 : 0.5 : 1"
+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+       id="perspective6119" />
+    <clipPath
+       id="clipPath3296"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path3298"
+         d="m 476.153,731.751 42.113,0 0,36.975 -42.113,0 0,-36.975 z" />
+    </clipPath>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="1 : 0.5 : 1"
+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+       id="perspective6219" />
+    <clipPath
+       id="clipPath2860"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path2862"
+         d="m 533.165,731.751 42.113,0 0,36.975 -42.113,0 0,-36.975 z" />
+    </clipPath>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="1 : 0.5 : 1"
+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+       id="perspective6281" />
+    <clipPath
+       id="clipPath3080"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path3082"
+         d="m 20.045,674.69 42.114,0 0,36.975 -42.114,0 0,-36.975 z" />
+    </clipPath>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="1 : 0.5 : 1"
+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+       id="perspective6343" />
+    <clipPath
+       id="clipPath2964"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path2966"
+         d="m 77.057,674.69 42.114,0 0,36.975 -42.114,0 0,-36.975 z" />
+    </clipPath>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="1 : 0.5 : 1"
+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+       id="perspective6432" />
+    <clipPath
+       id="clipPath3256"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path3258"
+         d="m 134.072,674.69 42.114,0 0,36.975 -42.114,0 0,-36.975 z" />
+    </clipPath>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="1 : 0.5 : 1"
+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+       id="perspective6560" />
+    <clipPath
+       id="clipPath2908"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path2910"
+         d="m 191.084,674.69 42.113,0 0,36.975 -42.113,0 0,-36.975 z" />
+    </clipPath>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="1 : 0.5 : 1"
+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+       id="perspective6650" />
+    <clipPath
+       id="clipPath3128"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path3130"
+         d="m 248.098,674.69 42.114,0 0,36.975 -42.114,0 0,-36.975 z" />
+    </clipPath>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="1 : 0.5 : 1"
+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+       id="perspective6920" />
+    <clipPath
+       id="clipPath3016"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path3018"
+         d="m 305.111,674.69 42.114,0 0,36.975 -42.114,0 0,-36.975 z" />
+    </clipPath>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="1 : 0.5 : 1"
+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+       id="perspective6991" />
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="1 : 0.5 : 1"
+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+       id="perspective7016" />
+    <clipPath
+       id="clipPath4234"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path4236"
+         d="m 362.126,674.673 42.113,0 0,36.975 -42.113,0 0,-36.975 z" />
+    </clipPath>
+    <clipPath
+       id="clipPath4222"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path4224"
+         d="m 362.126,674.673 42.114,0 0,36.975 -42.114,0 0,-36.975 z" />
+    </clipPath>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="1 : 0.5 : 1"
+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+       id="perspective7091" />
+    <clipPath
+       id="clipPath4394"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path4396"
+         d="m 419.137,674.673 42.114,0 0,36.975 -42.114,0 0,-36.975 z" />
+    </clipPath>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="1 : 0.5 : 1"
+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+       id="perspective7169" />
+    <clipPath
+       id="clipPath4848"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path4850"
+         d="m 476.152,674.673 42.114,0 0,36.975 -42.114,0 0,-36.975 z" />
+    </clipPath>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="1 : 0.5 : 1"
+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+       id="perspective7231" />
+    <clipPath
+       id="clipPath4976"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path4978"
+         d="m 20.044,617.671 42.114,0 0,36.975 -42.114,0 0,-36.975 z" />
+    </clipPath>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="1 : 0.5 : 1"
+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+       id="perspective7293" />
+    <clipPath
+       id="clipPath4924"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path4926"
+         d="m 134.07,617.671 42.114,0 0,36.975 -42.114,0 0,-36.975 z" />
+    </clipPath>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="1 : 0.5 : 1"
+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+       id="perspective7355" />
+    <clipPath
+       id="clipPath4732"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path4734"
+         d="m 191.083,617.671 42.114,0 0,36.975 -42.114,0 0,-36.975 z" />
+    </clipPath>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="1 : 0.5 : 1"
+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+       id="perspective7417" />
+    <clipPath
+       id="clipPath4538"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path4540"
+         d="m 305.109,617.68 42.114,0 0,36.966 -42.114,0 0,-36.966 z" />
+    </clipPath>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="1 : 0.5 : 1"
+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+       id="perspective7567" />
+    <clipPath
+       id="clipPath4888"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path4890"
+         d="m 362.141,617.646 42.114,0 0,36.974 -42.114,0 0,-36.974 z" />
+    </clipPath>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="1 : 0.5 : 1"
+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+       id="perspective7715" />
+    <clipPath
+       id="clipPath4704"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path4706"
+         d="m 419.137,617.646 42.114,0 0,36.974 -42.114,0 0,-36.974 z" />
+    </clipPath>
+    <clipPath
+       id="clipPath4684"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path4686"
+         d="m 419.137,617.646 42.114,0 0,36.974 -42.114,0 0,-36.974 z" />
+    </clipPath>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="1 : 0.5 : 1"
+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+       id="perspective7822" />
+    <clipPath
+       id="clipPath5076"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path5078"
+         d="m 476.152,617.644 42.135,0 0,37.002 -42.135,0 0,-37.002 z" />
+    </clipPath>
+    <clipPath
+       id="clipPath5064"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path5066"
+         d="m 476.152,617.645 42.135,0 0,37.001 -42.135,0 0,-37.001 z" />
+    </clipPath>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 0.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="1 : 0.5 : 1"
+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+       id="perspective7961" />
+    <clipPath
+       id="clipPath5016"
+       clipPathUnits="userSpaceOnUse">
+      <path
+         id="path5018"
+         d="m 533.182,617.715 42.114,0 0,36.931 -42.114,0 0,-36.931 z" />
+    </clipPath>
+  </defs>
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="1920"
+     inkscape:window-height="1029"
+     id="namedview889"
+     showgrid="false"
+     inkscape:snap-object-midpoints="true"
+     inkscape:zoom="1.4146341"
+     inkscape:cx="313.67434"
+     inkscape:cy="-16.558956"
+     inkscape:window-x="0"
+     inkscape:window-y="25"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg887" />
+  <flowRoot
+     style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:16px;line-height:125%;font-family:'Open Sans';-inkscape-font-specification:'Open Sans';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+     id="flowRoot895"
+     xml:space="preserve"><flowRegion
+       id="flowRegion897"><rect
+         y="52.517094"
+         x="264.42136"
+         height="49.182903"
+         width="35.07864"
+         id="rect899" /></flowRegion><flowPara
+       id="flowPara901" /></flowRoot>  <path
+     inkscape:connector-curvature="0"
+     style="fill:#ff0000"
+     d="m 152.57107,17.366977 a 19.6,19.6 0 0 0 -13.8,-13.8000005 c -12.1,-3.29999996 -60.800012,-3.29999996 -60.800012,-3.29999996 0,0 -48.7,0.1 -60.79999,3.39999996 a 19.6,19.6 0 0 0 -13.8,13.8000005 c -3.65999997,21.5 -5.08,54.259998 0.1,74.899998 a 19.6,19.6 0 0 0 13.8,13.799995 c 12.09999,3.3 60.79999,3.3 60.79999,3.3 0,0 48.700012,0 60.800012,-3.3 a 19.6,19.6 0 0 0 13.8,-13.799995 c 3.86,-21.53 5.05,-54.269998 -0.1,-74.999998 z"
+     id="path874-3" />
+  <path
+     inkscape:connector-curvature="0"
+     style="fill:#ffffff"
+     d="m -79.606089,-244.68605 40.40001,-23.4 -40.40001,-23.4 z"
+     id="path876-6" />
+  <path
+     inkscape:connector-curvature="0"
+     id="path4208"
+     style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+     d="m -581.85979,-130.89141 c -3.40816,-5.44927 -3.05691,-12.59787 0,-17.70537 l 181.81943,-315.27899 c 3.39867,-5.43977 9.1897,-9.18971 15.65477,-9.18971 6.46507,0 12.25611,3.74994 15.32251,8.85744 l 182.1517,315.26949 c 3.0664,5.44927 3.39867,12.59787 0,18.04714 -3.0664,5.79103 -9.18971,9.1992 -15.3225,9.1992 h -364.30341 c -6.1328,0 -12.26559,-3.40817 -15.3225,-9.1992" />
+  <g
+     inkscape:label="Ebene 1"
+     inkscape:groupmode="layer"
+     transform="translate(-895.54691,-44.45542)"
+     id="layer1-0" />
+  <g
+     style="fill:#ffffff"
+     id="g8075-7"
+     transform="matrix(-0.52072048,0,0,0.52072048,81.257728,-156.77219)">
+    <g
+       style="fill:#ffffff"
+       transform="matrix(9.6802871,0,0,-9.6802871,76.943493,438.0967)"
+       id="g5024-5">
+      <path
+         inkscape:connector-curvature="0"
+         id="path5026-3"
+         style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none"
+         d="m 0,0 c 0,-0.105 0.035,-0.245 0.035,-0.35 0,-1.154 -0.944,-2.064 -2.065,-2.064 -1.119,0 -2.064,0.91 -2.064,2.064 0,0.105 0.036,0.245 0.036,0.35 z" />
+    </g>
+    <g
+       style="fill:#ffffff"
+       transform="matrix(9.6802871,0,0,-9.6802871,76.943493,438.0967)"
+       id="g5028-5">
+      <path
+         inkscape:connector-curvature="0"
+         id="path5030-6"
+         style="fill:#ffffff;stroke:#000000;stroke-width:0.01431872;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2.61299992;stroke-dasharray:none;stroke-opacity:1"
+         d="m 0,0 c 0,-0.105 0.035,-0.245 0.035,-0.35 0,-1.154 -0.944,-2.064 -2.065,-2.064 -1.119,0 -2.064,0.91 -2.064,2.064 0,0.105 0.036,0.245 0.036,0.35 z" />
+    </g>
+    <g
+       style="fill:#ffffff"
+       transform="matrix(9.6802871,0,0,-9.6802871,25.803503,438.0967)"
+       id="g5032-2">
+      <path
+         inkscape:connector-curvature="0"
+         id="path5034-9"
+         style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none"
+         d="m 0,0 c 0,-0.105 0.035,-0.245 0.035,-0.35 0,-1.154 -0.944,-2.064 -2.065,-2.064 -1.119,0 -2.064,0.91 -2.064,2.064 0,0.105 0.036,0.245 0.036,0.35 z" />
+    </g>
+    <g
+       style="fill:#ffffff"
+       transform="matrix(9.6802871,0,0,-9.6802871,25.803503,438.0967)"
+       id="g5036-1">
+      <path
+         inkscape:connector-curvature="0"
+         id="path5038-2"
+         style="fill:#ffffff;stroke:#000000;stroke-width:0.01431872;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2.61299992;stroke-dasharray:none;stroke-opacity:1"
+         d="m 0,0 c 0,-0.105 0.035,-0.245 0.035,-0.35 0,-1.154 -0.944,-2.064 -2.065,-2.064 -1.119,0 -2.064,0.91 -2.064,2.064 0,0.105 0.036,0.245 0.036,0.35 z" />
+    </g>
+    <g
+       style="fill:#ffffff"
+       transform="matrix(9.6802871,0,0,-9.6802871,-25.338427,438.0967)"
+       id="g5040-7">
+      <path
+         inkscape:connector-curvature="0"
+         id="path5042-0"
+         style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none"
+         d="m 0,0 c 0.035,-0.105 0.035,-0.245 0.035,-0.35 0,-1.154 -0.91,-2.064 -2.065,-2.064 -1.119,0 -2.064,0.91 -2.064,2.064 0,0.105 0.036,0.245 0.036,0.35 z" />
+    </g>
+    <g
+       style="fill:#ffffff"
+       transform="matrix(9.6802871,0,0,-9.6802871,-25.338427,438.0967)"
+       id="g5044-9">
+      <path
+         inkscape:connector-curvature="0"
+         id="path5046-3"
+         style="fill:#ffffff;stroke:#000000;stroke-width:0.01431872;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2.61299992;stroke-dasharray:none;stroke-opacity:1"
+         d="m 0,0 c 0.035,-0.105 0.035,-0.245 0.035,-0.35 0,-1.154 -0.91,-2.064 -2.065,-2.064 -1.119,0 -2.064,0.91 -2.064,2.064 0,0.105 0.036,0.245 0.036,0.35 z" />
+    </g>
+    <g
+       style="fill:#ffffff"
+       transform="matrix(9.6802871,0,0,-9.6802871,6.160263,373.06937)"
+       id="g5048-6">
+      <path
+         inkscape:connector-curvature="0"
+         id="path5050-0"
+         style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none"
+         d="M 0,0 C 0,0.77 -0.56,1.399 -1.329,1.399 -2.1,1.399 -2.659,0.77 -2.659,0 v -0.7 c 0,-0.139 -0.14,-0.279 -0.28,-0.279 h -2.064 c -0.14,0 -0.28,0.14 -0.28,0.279 v 1.855 c 0,0.104 -0.07,0.21 -0.21,0.21 h -1.294 c -0.14,0 -0.21,-0.106 -0.21,-0.21 V -0.7 c 0,-0.139 -0.14,-0.279 -0.28,-0.279 h -0.56 c -0.175,0 -0.315,-0.14 -0.315,-0.28 v -4.653 c 0,-0.176 0.14,-0.316 0.315,-0.316 H 7.802 c 0.175,0 0.315,0.14 0.315,0.316 v 3.323 c 0,0.175 -0.14,0.315 -0.315,0.315 H 5.563 c -0.14,0 -0.28,0.104 -0.28,0.28 V 0.28 c 0,0.175 0.14,0.28 0.28,0.28 h 2.169 c 0.245,0 0.42,0.175 0.42,0.385 0,0.21 -0.175,0.42 -0.42,0.42 H 2.939 C 2.763,1.365 2.624,1.225 2.624,1.049 V -0.7 c 0,-0.139 -0.14,-0.279 -0.28,-0.279 H 0.28 C 0.14,-0.979 0,-0.839 0,-0.7 Z" />
+    </g>
+  </g>
+  <text
+     xml:space="preserve"
+     style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:128px;line-height:125%;font-family:'Kenyan Coffee';-inkscape-font-specification:'Kenyan Coffee';letter-spacing:0px;word-spacing:0px;fill:#282828;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+     x="164.95093"
+     y="101.50253"
+     id="text966"><tspan
+       sodipodi:role="line"
+       id="tspan964"
+       x="164.95093"
+       y="101.50253">ChuChube</tspan></text>
+</svg>

BIN
frontend/chuchufe/src/img/no_thumbnail.png


+ 105 - 0
frontend/chuchufe/src/img/no_thumbnail.svg

@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="120"
+   height="90"
+   viewBox="0 0 31.749999 23.812501"
+   version="1.1"
+   id="svg8"
+   inkscape:version="0.92.3 (2405546, 2018-03-11)"
+   sodipodi:docname="no_thumbnail.svg"
+   inkscape:export-filename="/home/niels/drive/local/files/projects/programming/chu-chube/static/img/no_thumbnail.png"
+   inkscape:export-xdpi="96"
+   inkscape:export-ydpi="96">
+  <defs
+     id="defs2" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="3.3234019"
+     inkscape:cx="33.442598"
+     inkscape:cy="65.217851"
+     inkscape:document-units="mm"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     units="px"
+     inkscape:snap-page="true"
+     showguides="false"
+     inkscape:window-width="1920"
+     inkscape:window-height="1029"
+     inkscape:window-x="0"
+     inkscape:window-y="25"
+     inkscape:window-maximized="1" />
+  <metadata
+     id="metadata5">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-273.18748)">
+    <rect
+       style="opacity:1;fill:none;fill-opacity:0.0097561;fill-rule:evenodd;stroke:#ffffff;stroke-width:2;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0"
+       id="rect819"
+       width="28.750002"
+       height="20.8125"
+       x="1.5"
+       y="274.6875" />
+    <path
+       style="opacity:1;fill:none;fill-opacity:0.03902438;fill-rule:evenodd;stroke:#adadad;stroke-width:1;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       d="m 1.5,279.89062 v -2.60156 -2.60156 h 3.59375 3.593751"
+       id="rect819-6"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="ccccc" />
+    <path
+       style="opacity:1;fill:none;fill-opacity:0.03902438;fill-rule:evenodd;stroke:#adadad;stroke-width:1;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       d="m 30.250002,290.29688 v 2.60156 2.60156 h -3.59375 -3.593751"
+       id="rect819-6-7"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="ccccc" />
+    <path
+       style="opacity:1;fill:none;fill-opacity:0.03902438;fill-rule:evenodd;stroke:#adadad;stroke-width:1;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       d="m 30.250002,279.89062 v -2.60156 -2.60156 h -3.59375 -3.593751"
+       id="rect819-6-7-5"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="ccccc" />
+    <path
+       style="opacity:1;fill:none;fill-opacity:0.03902438;fill-rule:evenodd;stroke:#adadad;stroke-width:1;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       d="M 1.5,290.29688 V 292.89844 295.5 H 5.09375 8.687501"
+       id="rect819-6-3"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="ccccc" />
+    <text
+       xml:space="preserve"
+       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13.75833321px;line-height:125%;font-family:monospace;-inkscape-font-specification:monospace;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#adadad;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       x="15.778584"
+       y="290.00342"
+       id="text901"><tspan
+         sodipodi:role="line"
+         id="tspan899"
+         x="15.778584"
+         y="290.00342"
+         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'Kenyan Coffee';-inkscape-font-specification:'Kenyan Coffee';fill:#adadad;fill-opacity:1;stroke-width:0.26458332px">?</tspan></text>
+  </g>
+</svg>

+ 1 - 1
frontend/chuchufe/src/index.js

@@ -1,6 +1,6 @@
 import React from 'react';
 import ReactDOM from 'react-dom';
-import './css/index.css';
+import './css/player.css';
 import App from './App';
 // import * as serviceWorker from './serviceWorker';
 

+ 0 - 87
frontend/chuchufe/src/useRoom.js

@@ -1,87 +0,0 @@
-import { defaultRoom } from "./Room";
-import { useEffect, useRef, useState } from "react";
-import { makeMessage, Resolver } from "./Resolver";
-import { MessageTypes, PlayerState } from "./enums";
-
-
-export default function useRoom(path) {
-    const [room, setRoom] = useState(defaultRoom);
-
-    const resolver = useRef(new Resolver());
-
-    useEffect(() => {
-        resolver.current.register(MessageTypes.STATE, stateProcessor);
-        const ws = resolver.current.connectSocket(path);
-        ws.addEventListener("open", function () {
-            ws.send(makeMessage(MessageTypes.STATE, null))
-        });
-        ws.addEventListener("message", console.log);
-        return (() => ws.close)
-    }, [])
-
-    useEffect(() => {
-        const currentResolver = resolver.current;
-        if (currentResolver.websocket !== null) {
-            const handler = (event) => currentResolver.resolve(
-                currentResolver.websocket,
-                event.data,
-                [room, setRoom]
-            )
-            currentResolver.websocket.addEventListener('message', handler);
-            return () => currentResolver.websocket.removeEventListener('message', handler)
-        }
-    }, [room])
-
-
-    return [room, setRoom]
-}
-
-function stateProcessor(ws, data, [{ chueue, playback, controller, ...rest }, setRoom]) {
-    const { playing, state, lists } = data;
-    const { next, previous } = lists
-
-    const newRoom = {
-        chueue: {
-            queue: next,
-            queuePlayed: previous,
-            repeatEnabled: !!previous,
-        },
-        playback: {
-            song: playing,
-            state: state,
-        },
-        controller: {
-            isMe: false,
-        }
-
-    }
-
-    const codes = new Set()
-    for (const song of next) {
-        const { code } = song
-        codes.add(code)
-    }
-
-    for (const song of previous || []) {
-        const { code } = song;
-        codes.add(code)
-    }
-
-    if (codes.length > 0) {
-        ws.send(makeMessage(MessageTypes.SEARCH_ID, { id: codes }))
-    }
-
-    if (playing !== null) {
-        if (state === PlayerState.PLAYING) {
-            // Playback.play(playing) // TODO
-        } else {
-            // Playback.load(playing)
-        }
-    }
-
-    setRoom(newRoom);
-    // if (isLeader === null) {
-    //     setLeader(false) // TODO
-    // }
-    // afterStateInit()
-}

+ 1 - 1
frontend/chuchufe/src/Resolver.js → frontend/chuchufe/src/util/Resolver.js

@@ -10,7 +10,6 @@ export class Resolver {
     }
 
     register(message, handler) {
-        console.log("register", message, handler)
         this.registerMap.set(message, handler)
     }
 
@@ -47,6 +46,7 @@ export class Resolver {
 
     connectSocket(path) {
         this.websocket = new WebSocket(`wss://${HOST}:${PORT}/${path}`)
+        window.socket = this.websocket;
         return this.websocket
     }
 

+ 9 - 0
frontend/chuchufe/src/enums.js → frontend/chuchufe/src/util/enums.js

@@ -38,3 +38,12 @@ export const YoutubeResourceType = {
     PLAYLIST: "youtube#playlist",
     CHANNEL: "youtube#channel",
 }
+
+export const YoutubePlayerState = {
+    UNSTARTED: -1,
+    ENDED: 0,
+    PLAYING: 1,
+    PAUSED: 2,
+    BUFFERING: 3,
+    VIDEO_CUED: 5,
+}

+ 11 - 0
frontend/chuchufe/src/util/processors/controlProcessor.js

@@ -0,0 +1,11 @@
+
+export default function controlProcessor(ws, data, [{ controller, ...rest }, setRoom]) {
+    const {obtain} = data;
+    const {isMe} = controller;
+    if (obtain !== isMe) {
+        setRoom({
+            controller: {...controller, isMe: obtain},
+            ...rest
+        })
+    }
+}

+ 66 - 0
frontend/chuchufe/src/util/processors/listOperationProcessor.js

@@ -0,0 +1,66 @@
+import { ListOperationTypes, MessageTypes, PlayerState } from "../enums";
+import { Chueue } from "../room";
+import { makeMessage } from "../Resolver";
+
+export default function processListOperation(ws, data, [{ chueue, ...rest }, setRoom]) {
+    const { op, items } = data;
+    let newChueue;
+    switch (op) {
+        case ListOperationTypes.ADD:
+            newChueue = Chueue.add(chueue, items.map(({ code, id }) => ({ code, id })))
+
+            const {playback, videoInfoMap} = rest;
+
+            const newPlayback = {}
+
+            if (playback.song === null) {
+                const res = Chueue.pop(newChueue)
+                newChueue = res.chueue;
+                newPlayback.song = res.song;
+                newPlayback.state = PlayerState.PLAYING; // TODO Deprecate
+            }
+
+            const newVideoInfoMap = new Map(videoInfoMap)
+
+            const noCodeInfo = []
+            for (const { code, snippet } of items) {
+                if (snippet !== undefined) {
+                    newVideoInfoMap.set(code, snippet);
+                } else if (!rest.videoInfoMap.has(code)) {
+                    noCodeInfo.push(code)
+                }
+            }
+
+            setRoom({
+                chueue: {...chueue, ...newChueue},
+                ...rest,
+                videoInfoMap: newVideoInfoMap,
+                playback: {...playback, ...newPlayback},
+            })
+
+            if (noCodeInfo.length > 0) {
+                ws.send(makeMessage(MessageTypes.SEARCH_ID, { id: noCodeInfo }))
+            }
+            break;
+        case ListOperationTypes.DEL:
+            setRoom({
+                chueue: Chueue.remove(chueue, items.map(({ id }) => id)),
+                ...rest
+            })
+            break;
+        case ListOperationTypes.MOVE:
+            newChueue = chueue;
+            for (const { id, displacement } of items) {
+                newChueue = Chueue.move(newChueue, displacement, id)
+            }
+            setRoom({
+                chueue: newChueue,
+                ...rest,
+            })
+            break;
+        default:
+            console.error("Unknown ListOperationType", op);
+            break;
+    }
+
+}

+ 38 - 0
frontend/chuchufe/src/util/processors/mediaActionProcessor.js

@@ -0,0 +1,38 @@
+import { MediaAction, PlayerState } from "../enums";
+import { Chueue } from "../room";
+
+export default function mediaActionProcessor(ws, data, [{ chueue, playback, ...rest }, setRoom]) {
+    const { action, ended_id, enable } = data;
+    const { repeatEnabled } = chueue;
+    const { state, song } = playback;
+    const newPlayback = {};
+    let newChueue;
+
+    if (action === MediaAction.PLAY && state === PlayerState.PAUSED) {
+        newPlayback.state = PlayerState.PLAYING;
+    } else if (action === MediaAction.PAUSE && state === PlayerState.PLAYING) {
+        newPlayback.state = PlayerState.PAUSED;
+    } else if (action === MediaAction.NEXT) {
+        console.log(action, song, ended_id)
+        if (song !== null && song.id === ended_id) {
+            const res = Chueue.pop(chueue);
+            console.log("pop", res)
+            newPlayback.song = res.song;
+            newChueue = res.chueue;
+            if (res.song === null) {
+                newPlayback.state = PlayerState.LIST_END; // TODO deprecated
+            }
+        }
+    } else if (action === MediaAction.REPEAT) {
+        if (enable !== repeatEnabled) {
+            newChueue = Chueue.setRepeatEnabled(chueue, enable, song)
+        }
+    }
+
+    setRoom({
+        chueue: { ...chueue, ...newChueue },
+        playback: { ...playback, ...newPlayback },
+        ...rest,
+    })
+
+}

+ 7 - 0
frontend/chuchufe/src/util/processors/searchIdResultProcessor.js

@@ -0,0 +1,7 @@
+export default function searchIdResultProcessor(ws, { items }, [room, setRoom]) {
+    const newMap = new Map(room.videoInfoMap);
+    for (const { id: code, snippet } of items) {
+        newMap.set(code, snippet);
+    }
+    setRoom({...room, videoInfoMap: newMap});
+}

+ 38 - 0
frontend/chuchufe/src/util/processors/songEndProcessor.js

@@ -0,0 +1,38 @@
+import { PlayerState } from "../enums";
+import { Chueue } from "../room";
+
+const RTT_ESTIMATE = 1
+const ALLOWED_AHEAD = 5
+
+export default function songEndProcessor(ws, data, [{ playback, chueue, controller, ...rest }, setRoom]) {
+    const { ended_id, current_id } = data;
+    const { song } = playback;
+    const newPlayback = {};
+    let newChueue;
+    console.log(ended_id, current_id)
+    if (song === null) {
+        // Do nothing
+    } else if (ended_id === song.id) {
+        const res = Chueue.pop(chueue)
+        newChueue = res.chueue;
+        newPlayback.song = res.song;
+        if (res.song === null) {
+            newPlayback.state = PlayerState.LIST_END // TODO deprecated
+            // TODO SEEK TO END
+        }
+    } else if (current_id === song.id) {
+        // TODO better time sync
+        // if (!isMe && player.getCurrentTime() - RTT_ESTIMATE - ALLOWED_AHEAD > 0) {
+        //     player.seekTo(RTT_ESTIMATE + ALLOWED_AHEAD, true)
+        // }
+    } else {
+        console.error("Difficult state reached. Reset protocol not implemented. Either to far ahead, behind or state inconsistency", ended_id, current_id, videoPlaying)
+    }
+
+    return {
+        chueue: {...chueue, ...newChueue},
+        playback: {...playback, ...newPlayback},
+        controller, ...rest
+    }
+
+}

+ 52 - 0
frontend/chuchufe/src/util/processors/stateProcessor.js

@@ -0,0 +1,52 @@
+import { makeMessage } from "../Resolver";
+import { MessageTypes, PlayerState } from "../enums";
+
+export default function stateProcessor(ws, data, [{ chueue, playback, controller, ...rest }, setRoom]) {
+    const { playing, state, lists } = data;
+    const { next, previous } = lists
+
+    const newRoom = {
+        chueue: {
+            queue: next,
+            playedQueue: previous,
+            repeatEnabled: !!previous,
+        },
+        playback: {
+            song: playing,
+            state: state,
+        },
+        controller: {
+            isMe: false,
+        },
+        ...rest,
+    }
+
+    const codes = new Set()
+    for (const song of next) {
+        const { code } = song
+        codes.add(code)
+    }
+
+    for (const song of previous || []) {
+        const { code } = song;
+        codes.add(code)
+    }
+
+    if (codes.size > 0) {
+        ws.send(makeMessage(MessageTypes.SEARCH_ID, { id: Array.from(codes) }))
+    }
+
+    if (playing !== null) {
+        if (state === PlayerState.PLAYING) {
+            // Playback.play(playing) // TODO
+        } else {
+            // Playback.load(playing)
+        }
+    }
+
+    setRoom(newRoom);
+    // if (isLeader === null) {
+    //     setLeader(false) // TODO
+    // }
+    // afterStateInit()
+}

+ 34 - 8
frontend/chuchufe/src/Room.js → frontend/chuchufe/src/util/room.js

@@ -13,25 +13,26 @@ export const defaultRoom = {
     controller: {
         isMe: false,
     },
+    videoInfoMap: new Map(),
 }
 
 export const Chueue = {
-    add: function ({ queue, ...rest }, ...songs) {
+    add: function ({ queue, ...rest }, songs) {
         return {
-            queue: queue + songs,
+            queue: queue.concat(songs),
             ...rest
         }
     },
 
-    remove: function ({ queue, ...rest }, ...ids) {
+    remove: function ({ queue, ...rest }, ids) {
         return {
-            queue: queue.filter(({ id }) => !ids.contains(id)),
+            queue: queue.filter(({ id }) => !ids.includes(id)),
             ...rest
         }
     },
 
     move: function ({ queue, ...rest }, displacement, id) {
-        const i = queue.findIndex(({ qId }) => qId === id)
+        const i = queue.findIndex(({ id: qId }) => qId === id)
         const song = queue[i]
         const new_i = i + displacement
         const temp_queue = queue.slice(0, i).concat(queue.slice(i + 1))
@@ -45,14 +46,39 @@ export const Chueue = {
         if (queue.length <= 0) {
             if (repeatEnabled && playedQueue.length > 0) {
                 return {
-                    song: playedQueue[playedQueue.length - 1],
+                    song: playedQueue[0],
                     chueue: {
-                        queue: playedQueue.slice(0, -1),
-                        playedQueue: [],
+                        queue: playedQueue.slice(1),
+                        playedQueue: playedQueue.slice(0, 1),
                         repeatEnabled,
                         ...rest
                     }
                 }
+            } else {
+                return {
+                    song: null,
+                    chueue: { queue, playedQueue, repeatEnabled, ...rest }
+                }
+            }
+        } else {
+            if (repeatEnabled) {
+                return {
+                    song: queue[0],
+                    chueue: {
+                        queue: queue.slice(1),
+                        playedQueue: playedQueue.concat([queue[0]]),
+                        repeatEnabled,
+                        ...rest
+                    }
+                }
+            } else {
+                return {
+                    song: queue[0],
+                    chueue: {
+                        queue: queue.slice(1),
+                        playedQueue, repeatEnabled, ...rest
+                    }
+                }
             }
         }
     },

+ 0 - 0
frontend/chuchufe/src/serviceWorker.js → frontend/chuchufe/src/util/serviceWorker.js


+ 0 - 0
frontend/chuchufe/src/setupTests.js → frontend/chuchufe/src/util/setupTests.js


+ 27 - 0
frontend/chuchufe/src/util/useMapBuilder.js

@@ -0,0 +1,27 @@
+
+export default function MapBuilder(map, setMap) {
+    const newMap = new Map(map);
+
+    function addToMap(key, value) {
+        newMap.set(key, value);
+    }
+
+    function update() {
+        setMap(newMap);
+    }
+
+    function addToMapSave(key, value) {
+        console.log(key, value)
+        addToMap(key, value)
+        update()
+    }
+
+    function addMultipleToMapSave(items) {
+        for (const [key, value] of items) {
+            addToMap(key, value)
+        }
+        update();
+    }
+
+    return {addToMap, update, addToMapSave, addMultipleToMapSave}
+}

+ 84 - 0
frontend/chuchufe/src/util/useRoom.js

@@ -0,0 +1,84 @@
+import { defaultRoom } from "./room";
+import { useEffect, useRef, useState } from "react";
+import { makeMessage, Resolver } from "./Resolver";
+import { MessageTypes } from "./enums";
+import stateProcessor from "./processors/stateProcessor";
+import listOperationProcessor from "./processors/listOperationProcessor";
+import mediaActionProcessor from "./processors/mediaActionProcessor";
+import controlProcessor from "./processors/controlProcessor";
+import searchIdResultProcessor from "./processors/searchIdResultProcessor";
+
+function registerHandlers(resolver) {
+    resolver.register(MessageTypes.STATE, stateProcessor);
+    resolver.register(MessageTypes.LIST_OPERATION, listOperationProcessor);
+    resolver.register(MessageTypes.MEDIA_ACTION, mediaActionProcessor);
+    resolver.register(MessageTypes.OBTAIN_CONTROL, (ws, _, clientData) => controlProcessor(ws, { obtain: true }, clientData))
+    resolver.register(MessageTypes.RELEASE_CONTROL, (ws, _, clientData) => controlProcessor(ws, { obtain: false }, clientData))
+    resolver.register(MessageTypes.SEARCH_ID, searchIdResultProcessor)
+}
+
+function unRegisterHandlers(resolver) {
+    resolver.unregister(MessageTypes.STATE)
+    resolver.unregister(MessageTypes.LIST_OPERATION)
+    resolver.unregister(MessageTypes.MEDIA_ACTION)
+    resolver.unregister(MessageTypes.OBTAIN_CONTROL)
+    resolver.unregister(MessageTypes.RELEASE_CONTROL)
+    resolver.unregister(MessageTypes.SEARCH_ID)
+}
+
+
+export default function useRoom(path) {
+    const [room, setRoom] = useState({ ...defaultRoom });
+
+    const resolverRef = useRef(new Resolver());
+    const [connected, setConnected] = useState(false);
+
+    useEffect(() => {
+        if (path !== null) {
+            const ws = resolverRef.current.connectSocket(path);
+            ws.addEventListener("open", function () {
+                ws.send(makeMessage(MessageTypes.STATE, null))
+            });
+            ws.addEventListener("message", console.log);
+            setConnected(true);
+            return (() => {
+                ws.close();
+                setConnected(false);
+            })
+        }
+    }, [path])
+
+    useEffect(() => {
+        const currentResolver = resolverRef.current;
+        if (connected && currentResolver.websocket !== null) {
+            const handler = (event) => currentResolver.resolve(
+                currentResolver.websocket,
+                event.data,
+                [room, setRoom]
+            )
+            currentResolver.websocket.addEventListener('message', handler);
+            return () => currentResolver.websocket.removeEventListener('message', handler)
+        }
+    }, [room, connected])
+
+    useEffect(() => {
+        const resolver = resolverRef.current
+        registerHandlers(resolver)
+        return () => unRegisterHandlers(resolver)
+    }, [connected, resolverRef])
+
+    const resolver =resolverRef.current;
+    const websocket = resolver.websocket;
+
+    return {
+        room,
+        setRoom,
+        resolver,
+        socket: {
+            connected,
+            ws: websocket,
+            send: websocket ? websocket.send : (...r) => console.error("websocket is not created yet. please do no try to send anything already. have some patience", r)
+        }
+    }
+}
+

+ 0 - 0
static/webfonts/fa-brands-400.eot → frontend/chuchufe/src/webfonts/fa-brands-400.eot


+ 0 - 0
static/webfonts/fa-brands-400.svg → frontend/chuchufe/src/webfonts/fa-brands-400.svg


+ 0 - 0
static/webfonts/fa-brands-400.ttf → frontend/chuchufe/src/webfonts/fa-brands-400.ttf


+ 0 - 0
static/webfonts/fa-brands-400.woff → frontend/chuchufe/src/webfonts/fa-brands-400.woff


+ 0 - 0
static/webfonts/fa-brands-400.woff2 → frontend/chuchufe/src/webfonts/fa-brands-400.woff2


+ 0 - 0
static/webfonts/fa-regular-400.eot → frontend/chuchufe/src/webfonts/fa-regular-400.eot


+ 0 - 0
static/webfonts/fa-regular-400.svg → frontend/chuchufe/src/webfonts/fa-regular-400.svg


+ 0 - 0
static/webfonts/fa-regular-400.ttf → frontend/chuchufe/src/webfonts/fa-regular-400.ttf


+ 0 - 0
static/webfonts/fa-regular-400.woff → frontend/chuchufe/src/webfonts/fa-regular-400.woff


+ 0 - 0
static/webfonts/fa-regular-400.woff2 → frontend/chuchufe/src/webfonts/fa-regular-400.woff2


+ 0 - 0
static/webfonts/fa-solid-900.eot → frontend/chuchufe/src/webfonts/fa-solid-900.eot


+ 0 - 0
static/webfonts/fa-solid-900.svg → frontend/chuchufe/src/webfonts/fa-solid-900.svg


+ 0 - 0
static/webfonts/fa-solid-900.ttf → frontend/chuchufe/src/webfonts/fa-solid-900.ttf


+ 0 - 0
static/webfonts/fa-solid-900.woff → frontend/chuchufe/src/webfonts/fa-solid-900.woff


+ 0 - 0
static/webfonts/fa-solid-900.woff2 → frontend/chuchufe/src/webfonts/fa-solid-900.woff2


+ 0 - 0
static/webfonts/kenyc.ttf → frontend/chuchufe/src/webfonts/kenyc.ttf


+ 5 - 65
frontend/chuchufe/yarn.lock

@@ -4754,11 +4754,6 @@ fs.realpath@^1.0.0:
   resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
   integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
 
-fsevents@2.1.2, fsevents@~2.1.2:
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.2.tgz#4c0a1fb34bc68e543b4b82a9ec392bfbda840805"
-  integrity sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA==
-
 fsevents@^1.2.7:
   version "1.2.12"
   resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.12.tgz#db7e0d8ec3b0b45724fd4d83d43554a8f1f0de5c"
@@ -4767,6 +4762,11 @@ fsevents@^1.2.7:
     bindings "^1.5.0"
     nan "^2.12.1"
 
+fsevents@~2.1.2:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.2.tgz#4c0a1fb34bc68e543b4b82a9ec392bfbda840805"
+  integrity sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA==
+
 function-bind@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
@@ -8620,66 +8620,6 @@ react-is@^16.12.0, react-is@^16.8.1, react-is@^16.8.4:
   resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
   integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
 
-react-scripts@3.4.3:
-  version "3.4.3"
-  resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-3.4.3.tgz#21de5eb93de41ee92cd0b85b0e1298d0bb2e6c51"
-  integrity sha512-oSnoWmii/iKdeQiwaO6map1lUaZLmG0xIUyb/HwCVFLT7gNbj8JZ9RmpvMCZ4fB98ZUMRfNmp/ft8uy/xD1RLA==
-  dependencies:
-    "@babel/core" "7.9.0"
-    "@svgr/webpack" "4.3.3"
-    "@typescript-eslint/eslint-plugin" "^2.10.0"
-    "@typescript-eslint/parser" "^2.10.0"
-    babel-eslint "10.1.0"
-    babel-jest "^24.9.0"
-    babel-loader "8.1.0"
-    babel-plugin-named-asset-import "^0.3.6"
-    babel-preset-react-app "^9.1.2"
-    camelcase "^5.3.1"
-    case-sensitive-paths-webpack-plugin "2.3.0"
-    css-loader "3.4.2"
-    dotenv "8.2.0"
-    dotenv-expand "5.1.0"
-    eslint "^6.6.0"
-    eslint-config-react-app "^5.2.1"
-    eslint-loader "3.0.3"
-    eslint-plugin-flowtype "4.6.0"
-    eslint-plugin-import "2.20.1"
-    eslint-plugin-jsx-a11y "6.2.3"
-    eslint-plugin-react "7.19.0"
-    eslint-plugin-react-hooks "^1.6.1"
-    file-loader "4.3.0"
-    fs-extra "^8.1.0"
-    html-webpack-plugin "4.0.0-beta.11"
-    identity-obj-proxy "3.0.0"
-    jest "24.9.0"
-    jest-environment-jsdom-fourteen "1.0.1"
-    jest-resolve "24.9.0"
-    jest-watch-typeahead "0.4.2"
-    mini-css-extract-plugin "0.9.0"
-    optimize-css-assets-webpack-plugin "5.0.3"
-    pnp-webpack-plugin "1.6.4"
-    postcss-flexbugs-fixes "4.1.0"
-    postcss-loader "3.0.0"
-    postcss-normalize "8.0.1"
-    postcss-preset-env "6.7.0"
-    postcss-safe-parser "4.0.1"
-    react-app-polyfill "^1.0.6"
-    react-dev-utils "^10.2.1"
-    resolve "1.15.0"
-    resolve-url-loader "3.1.1"
-    sass-loader "8.0.2"
-    semver "6.3.0"
-    style-loader "0.23.1"
-    terser-webpack-plugin "2.3.8"
-    ts-pnp "1.1.6"
-    url-loader "2.3.0"
-    webpack "4.42.0"
-    webpack-dev-server "3.11.0"
-    webpack-manifest-plugin "2.2.0"
-    workbox-webpack-plugin "4.3.1"
-  optionalDependencies:
-    fsevents "2.1.2"
-
 react@^16.14.0:
   version "16.14.0"
   resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d"

+ 1 - 1
static/css/main.css

@@ -1,6 +1,6 @@
 @font-face {
     font-family: "Kenyan Coffee";
-    src: url("../webfonts/kenyc.ttf");
+    src: url("drive/local/files/projects/programming/chu-chube/frontend/chuchufe/src/webfonts/kenyc.ttf");
 }
 
 @media (min-width: 1500px) {

+ 2 - 2
static/css/regular.css

@@ -7,8 +7,8 @@
   font-style: normal;
   font-weight: 400;
   font-display: block;
-  src: url("../webfonts/fa-regular-400.eot");
-  src: url("../webfonts/fa-regular-400.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.woff") format("woff"), url("../webfonts/fa-regular-400.ttf") format("truetype"), url("../webfonts/fa-regular-400.svg#fontawesome") format("svg"); }
+  src: url("drive/local/files/projects/programming/chu-chube/frontend/chuchufe/src/webfonts/fa-regular-400.eot");
+  src: url("drive/local/files/projects/programming/chu-chube/frontend/chuchufe/src/webfonts/fa-regular-400.eot?#iefix") format("embedded-opentype"), url("drive/local/files/projects/programming/chu-chube/frontend/chuchufe/src/webfonts/fa-regular-400.woff2") format("woff2"), url("drive/local/files/projects/programming/chu-chube/frontend/chuchufe/src/webfonts/fa-regular-400.woff") format("woff"), url("drive/local/files/projects/programming/chu-chube/frontend/chuchufe/src/webfonts/fa-regular-400.ttf") format("truetype"), url("drive/local/files/projects/programming/chu-chube/frontend/chuchufe/src/webfonts/fa-regular-400.svg#fontawesome") format("svg"); }
 
 .far {
   font-family: 'Font Awesome 5 Free';

+ 1 - 1
static/css/regular.min.css

@@ -2,4 +2,4 @@
  * Font Awesome Free 5.14.0 by @fontawesome - https://fontawesome.com
  * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
  */
-@font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-regular-400.eot);src:url(../webfonts/fa-regular-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.woff) format("woff"),url(../webfonts/fa-regular-400.ttf) format("truetype"),url(../webfonts/fa-regular-400.svg#fontawesome) format("svg")}.far{font-family:"Font Awesome 5 Free";font-weight:400}
+@font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:400;font-display:block;src:url(drive/local/files/projects/programming/chu-chube/frontend/chuchufe/src/webfonts/fa-regular-400.eot);src:url(drive/local/files/projects/programming/chu-chube/frontend/chuchufe/src/webfonts/fa-regular-400.eot?#iefix) format("embedded-opentype"),url(drive/local/files/projects/programming/chu-chube/frontend/chuchufe/src/webfonts/fa-regular-400.woff2) format("woff2"),url(drive/local/files/projects/programming/chu-chube/frontend/chuchufe/src/webfonts/fa-regular-400.woff) format("woff"),url(drive/local/files/projects/programming/chu-chube/frontend/chuchufe/src/webfonts/fa-regular-400.ttf) format("truetype"),url(drive/local/files/projects/programming/chu-chube/frontend/chuchufe/src/webfonts/fa-regular-400.svg#fontawesome) format("svg")}.far{font-family:"Font Awesome 5 Free";font-weight:400}

+ 2 - 2
static/css/solid.css

@@ -7,8 +7,8 @@
   font-style: normal;
   font-weight: 900;
   font-display: block;
-  src: url("../webfonts/fa-solid-900.eot");
-  src: url("../webfonts/fa-solid-900.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.woff") format("woff"), url("../webfonts/fa-solid-900.ttf") format("truetype"), url("../webfonts/fa-solid-900.svg#fontawesome") format("svg"); }
+  src: url("drive/local/files/projects/programming/chu-chube/frontend/chuchufe/src/webfonts/fa-solid-900.eot");
+  src: url("drive/local/files/projects/programming/chu-chube/frontend/chuchufe/src/webfonts/fa-solid-900.eot?#iefix") format("embedded-opentype"), url("drive/local/files/projects/programming/chu-chube/frontend/chuchufe/src/webfonts/fa-solid-900.woff2") format("woff2"), url("drive/local/files/projects/programming/chu-chube/frontend/chuchufe/src/webfonts/fa-solid-900.woff") format("woff"), url("drive/local/files/projects/programming/chu-chube/frontend/chuchufe/src/webfonts/fa-solid-900.ttf") format("truetype"), url("drive/local/files/projects/programming/chu-chube/frontend/chuchufe/src/webfonts/fa-solid-900.svg#fontawesome") format("svg"); }
 
 .fa,
 .fas {

+ 1 - 1
static/css/solid.min.css

@@ -2,4 +2,4 @@
  * Font Awesome Free 5.14.0 by @fontawesome - https://fontawesome.com
  * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
  */
-@font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-solid-900.eot);src:url(../webfonts/fa-solid-900.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.woff) format("woff"),url(../webfonts/fa-solid-900.ttf) format("truetype"),url(../webfonts/fa-solid-900.svg#fontawesome) format("svg")}.fa,.fas{font-family:"Font Awesome 5 Free";font-weight:900}
+@font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:900;font-display:block;src:url(drive/local/files/projects/programming/chu-chube/frontend/chuchufe/src/webfonts/fa-solid-900.eot);src:url(drive/local/files/projects/programming/chu-chube/frontend/chuchufe/src/webfonts/fa-solid-900.eot?#iefix) format("embedded-opentype"),url(drive/local/files/projects/programming/chu-chube/frontend/chuchufe/src/webfonts/fa-solid-900.woff2) format("woff2"),url(drive/local/files/projects/programming/chu-chube/frontend/chuchufe/src/webfonts/fa-solid-900.woff) format("woff"),url(drive/local/files/projects/programming/chu-chube/frontend/chuchufe/src/webfonts/fa-solid-900.ttf) format("truetype"),url(drive/local/files/projects/programming/chu-chube/frontend/chuchufe/src/webfonts/fa-solid-900.svg#fontawesome) format("svg")}.fa,.fas{font-family:"Font Awesome 5 Free";font-weight:900}

+ 17 - 17
static/js/main.js

@@ -56,7 +56,7 @@ function onPlayerStateChange(event) {
             return
         }
         if (isLeader && videoPlaying !== null) {
-            socket.send(makeMessage(MessageTypes.SONG_END, { id: videoPlaying.id }))
+            socket.ws.send(makeMessage(MessageTypes.SONG_END, { id: videoPlaying.id }))
         }
         const vid = popVideo()
         if (vid !== undefined) {
@@ -281,7 +281,7 @@ function removeQueueLine(id) {
 
 function onSubmit(event) {
     event.preventDefault();
-    socket.send(makeMessage(MessageTypes.LIST_OPERATION, {
+    socket.ws.send(makeMessage(MessageTypes.LIST_OPERATION, {
         op: ListOperationTypes.ADD,
         code: event.target[0].value
     }))
@@ -291,25 +291,25 @@ function onSearch(event) {
     event.preventDefault();
     const q = event.target[0].value
     if (q !== "") {
-        socket.send(makeMessage(MessageTypes.SEARCH, { q }))
+        socket.ws.send(makeMessage(MessageTypes.SEARCH, { q }))
     }
 }
 
 function onDeleteClick(event, id) {
     event.preventDefault();
-    socket.send(makeMessage(MessageTypes.LIST_OPERATION, { op: ListOperationTypes.DEL, id }))
+    socket.ws.send(makeMessage(MessageTypes.LIST_OPERATION, { op: ListOperationTypes.DEL, id }))
 }
 
 function onMoveClick(event, id, displacement) {
     event.preventDefault();
-    socket.send(makeMessage(MessageTypes.LIST_OPERATION, { op: ListOperationTypes.MOVE, id, displacement }))
+    socket.ws.send(makeMessage(MessageTypes.LIST_OPERATION, { op: ListOperationTypes.MOVE, id, displacement }))
 }
 
 function onLeaderbutton(event) {
     if (isLeader) {
-        socket.send(makeMessage(MessageTypes.RELEASE_CONTROL))
+        socket.ws.send(makeMessage(MessageTypes.RELEASE_CONTROL))
     } else {
-        socket.send(makeMessage(MessageTypes.OBTAIN_CONTROL))
+        socket.ws.send(makeMessage(MessageTypes.OBTAIN_CONTROL))
     }
 }
 
@@ -345,7 +345,7 @@ function onPlayerStart(event) {
             }
             break;
     }
-    socket.send(makeMessage(MessageTypes.PLAYER_ENABLED, { enabled: true }))
+    socket.ws.send(makeMessage(MessageTypes.PLAYER_ENABLED, { enabled: true }))
 }
 
 function onPlayerClose(event) {
@@ -356,7 +356,7 @@ function onPlayerClose(event) {
     if (player !== null) {
         player.pauseVideo();
     }
-    socket.send(makeMessage(MessageTypes.PLAYER_ENABLED, { enabled: false }))
+    socket.ws.send(makeMessage(MessageTypes.PLAYER_ENABLED, { enabled: false }))
 }
 
 function hidePlayerPlaceholder(event) {
@@ -373,19 +373,19 @@ function showPlayerPlaceholder(event) {
 
 function onPlayButton(event) {
     event.preventDefault();
-    socket.send(makeMessage(MessageTypes.MEDIA_ACTION, { action: MediaAction.PLAY }))
+    socket.ws.send(makeMessage(MessageTypes.MEDIA_ACTION, { action: MediaAction.PLAY }))
 }
 
 function onPauseButton(event) {
     event.preventDefault();
-    socket.send(makeMessage(MessageTypes.MEDIA_ACTION, { action: MediaAction.PAUSE }))
+    socket.ws.send(makeMessage(MessageTypes.MEDIA_ACTION, { action: MediaAction.PAUSE }))
 
 }
 
 function onNextButton(event) {
     event.preventDefault();
     if (videoPlaying !== null) {
-        socket.send(makeMessage(MessageTypes.MEDIA_ACTION, { action: MediaAction.NEXT, current_id: videoPlaying.id }))
+        socket.ws.send(makeMessage(MessageTypes.MEDIA_ACTION, { action: MediaAction.NEXT, current_id: videoPlaying.id }))
     }
 }
 
@@ -393,7 +393,7 @@ const repeatButton = document.getElementById('repeat-button');
 
 function onRepeatButton(event) {
     event.preventDefault();
-    socket.send(makeMessage(MessageTypes.MEDIA_ACTION, { action: MediaAction.REPEAT, enable: !repeat }))
+    socket.ws.send(makeMessage(MessageTypes.MEDIA_ACTION, { action: MediaAction.REPEAT, enable: !repeat }))
 }
 
 function stateProcessor(ws, data) {
@@ -423,7 +423,7 @@ function stateProcessor(ws, data) {
     }
 
     if (codes.length > 0) {
-        socket.send(makeMessage(MessageTypes.SEARCH_ID, { id: codes }))
+        socket.ws.send(makeMessage(MessageTypes.SEARCH_ID, { id: codes }))
     }
 
     if (videoPlaying !== null) {
@@ -456,7 +456,7 @@ function listOperationProcessor(ws, data) {
             addVideo(code, id);
         }
         if (noCodeInfo.length > 0) {
-            socket.send(makeMessage(MessageTypes.SEARCH_ID, { id: noCodeInfo.join(',') }))
+            socket.ws.send(makeMessage(MessageTypes.SEARCH_ID, { id: noCodeInfo.join(',') }))
         }
     } else if (op === ListOperationTypes.DEL) {
         for (const { id } of items) {
@@ -551,7 +551,7 @@ function makeSearchResult(item) {
 
 
     function onClickHandler() {
-        socket.send(makeMessage(MessageTypes.LIST_OPERATION, {
+        socket.ws.send(makeMessage(MessageTypes.LIST_OPERATION, {
             op: ListOperationTypes.ADD,
             kind,
             code
@@ -630,7 +630,7 @@ function onYTDone() {
     resolver.register(MessageTypes.SEARCH_ID, searchIdResultProcessor)
     socket = resolver.connectSocket()
     socket.addEventListener("open", function () {
-        socket.send(makeMessage(MessageTypes.STATE, null))
+        socket.ws.send(makeMessage(MessageTypes.STATE, null))
     })
 }