Decouple client from server

Add Vite dependency and remove server side rendering to make it
possible to run the client independently.
Remove webpack config, replace with the `Vite` build tool.

GitLab: #55
Change-Id: I3a05d2e86cf6cb0ab91e77b3696f393132137575
diff --git a/client/.env.development b/client/.env.development
new file mode 100644
index 0000000..2accbba
--- /dev/null
+++ b/client/.env.development
@@ -0,0 +1 @@
+ESLINT_NO_DEV_ERRORS=true
diff --git a/client/.eslintrc.json b/client/.eslintrc.json
index e94691b..977d4a0 100644
--- a/client/.eslintrc.json
+++ b/client/.eslintrc.json
@@ -4,11 +4,7 @@
       "version": "detect"
     }
   },
-  "extends": [
-    "plugin:react/recommended",
-    "plugin:react-hooks/recommended",
-    "../.eslintrc.cjs"
-  ],
+  "extends": ["plugin:react/recommended", "plugin:react-hooks/recommended", "../.eslintrc.cjs"],
   "rules": {
     "react-hooks/exhaustive-deps": "error",
     "react/display-name": "off",
diff --git a/client/.gitignore b/client/.gitignore
new file mode 100644
index 0000000..567609b
--- /dev/null
+++ b/client/.gitignore
@@ -0,0 +1 @@
+build/
diff --git a/client/README.md b/client/README.md
index 3f3af69..404ef57 100644
--- a/client/README.md
+++ b/client/README.md
@@ -1,7 +1,56 @@
 # jami-web client
+
 This is the client for jami-web
 
-## Translations
+## Available Scripts
+
+In the project directory, you can run:
+
+### `npm start`
+
+Runs the app in the development mode.<br />
+Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
+
+The page will reload if you make edits.<br />
+You will also see any lint errors in the console.
+
+### `npm test`
+
+Run all tests.<br />
+
+### `npm run build`
+
+Builds the app for production to the `build` folder.<br />
+It correctly bundles React in production mode and optimizes the build for the best performance.
+
+Your app is ready to be deployed!
+
+See the section about [deployment](https://vitejs.dev/guide/static-deploy.html) for more information.
+
+### `npm run start:prod`
+
+Preview the production build locally.
+
+### `npm run clean`
+
+Clean build files
+
+### `npm run lint`
+
+Verify that no lint errors are present. Use `npm run lint:fix` to fix some errors.
+
+### `npm run format`
+
+Format all files with prettier. Use `npm run format:check` to verify that all files are formatted without changing any.
+
+## `npm run extract-translations`
+
+Update the translation files.
+
 The translations are handled by [i18next](https://www.i18next.com/)
 
-Execute `npm run extract-translations` from the client's folder in order to update the translation files.
+## Learn More
+
+You can learn more in the [Vite documentation](https://vitejs.dev/guide/).
+
+To learn React, check out the [React documentation](https://reactjs.org/).
diff --git a/client/index.html b/client/index.html
new file mode 100644
index 0000000..f308177
--- /dev/null
+++ b/client/index.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8" />
+    <link rel="icon" href="/favicon.png" />
+    <meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width" />
+    <meta name="description" content="Jami Web" />
+    <title>Jami Web</title>
+  </head>
+
+  <body>
+    <noscript>You need to enable JavaScript to run this app.</noscript>
+    <script type="module" src="src/index.tsx"></script>
+    <div id="root"></div>
+  </body>
+</html>
diff --git a/client/package.json b/client/package.json
index 585e35f..6eb5ca7 100644
--- a/client/package.json
+++ b/client/package.json
@@ -2,11 +2,13 @@
   "name": "jami-web-client",
   "version": "0.1.0",
   "private": true,
-  "type": "module",
   "scripts": {
-    "build": "webpack",
+    "start": "vite",
+    "start:prod": "vite preview",
+    "build": "tsc && vite build",
     "clean": "rm -rf dist",
-    "lint": "eslint src",
+    "test": "jest src",
+    "lint": "eslint .",
     "lint:fix": "npm run lint -- --fix",
     "format": "prettier --write src",
     "format:check": "prettier --check src",
@@ -24,7 +26,6 @@
     ]
   },
   "dependencies": {
-    "@babel/runtime": "7.18.9",
     "@emotion/react": "^11.10.0",
     "@emotion/styled": "^11.10.0",
     "@mui/icons-material": "^5.10.3",
@@ -40,13 +41,11 @@
     "@types/jest": "^28.1.8",
     "axios": "^0.27.2",
     "dayjs": "^1.11.5",
-    "dotenv": "^16.0.2",
     "emoji-picker-react": "^3.6.1",
     "framer-motion": "^7.3.5",
     "i18next": "^21.9.2",
     "jami-web-common": "file:../common",
     "qrcode.react": "^3.1.0",
-    "raw-loader": "^4.0.2",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
     "react-emoji-render": "^1.2.4",
@@ -58,30 +57,17 @@
     "socket.io-client": "^4.5.2"
   },
   "devDependencies": {
-    "@babel/core": "^7.19.1",
-    "@babel/plugin-transform-runtime": "^7.19.1",
-    "@babel/preset-env": "^7.19.1",
-    "@babel/preset-react": "^7.18.6",
-    "@pmmmwh/react-refresh-webpack-plugin": "^0.5.7",
-    "@svgr/webpack": "^6.3.1",
     "@types/node": "^18.7.13",
     "@types/react": "^18.0.17",
     "@types/react-dom": "^18.0.6",
     "@types/react-modal": "^3.13.1",
-    "babel-loader": "^8.2.5",
-    "copy-webpack-plugin": "^11.0.0",
-    "css-loader": "^6.7.1",
+    "@vitejs/plugin-react": "^2.1.0",
     "eslint-plugin-react": "^7.31.8",
     "eslint-plugin-react-hooks": "^4.6.0",
-    "html-webpack-plugin": "^5.5.0",
     "i18next-parser": "^6.5.0",
-    "react-refresh": "^0.14.0",
     "sass": "^1.54.5",
-    "sass-loader": "^13.0.2",
-    "style-loader": "^3.3.1",
-    "ts-loader": "^9.3.1",
     "typescript": "^4.7.4",
-    "webpack": "^5.74.0",
-    "webpack-cli": "^4.10.0"
+    "vite": "^3.1.8",
+    "vite-plugin-svgr": "^2.2.2"
   }
 }
diff --git a/client/src/AuthManager.ts b/client/src/AuthManager.ts
index 29c5ffc..91ac981 100644
--- a/client/src/AuthManager.ts
+++ b/client/src/AuthManager.ts
@@ -61,14 +61,6 @@
 
     this.tasks = [];
     this.onAuthChanged = undefined;
-
-    // @ts-ignore
-    if (initData) {
-      console.log('Using static initData');
-      // @ts-ignore
-      this.setInitData(initData);
-      return;
-    }
   }
 
   isAuthenticated() {
diff --git a/client/src/components/ContactList.js b/client/src/components/ContactList.jsx
similarity index 98%
rename from client/src/components/ContactList.js
rename to client/src/components/ContactList.jsx
index 7f8269b..2aee98b 100644
--- a/client/src/components/ContactList.js
+++ b/client/src/components/ContactList.jsx
@@ -21,8 +21,8 @@
 import { useEffect, useState } from 'react';
 import Modal from 'react-modal';
 
-import { useAppDispatch, useAppSelector } from '../../redux/hooks';
 import authManager from '../AuthManager';
+import { useAppDispatch, useAppSelector } from '../redux/hooks';
 import ConversationAvatar from './ConversationAvatar';
 
 const customStyles = {
diff --git a/client/src/components/ConversationList.tsx b/client/src/components/ConversationList.tsx
index b70001a..9ad47f6 100644
--- a/client/src/components/ConversationList.tsx
+++ b/client/src/components/ConversationList.tsx
@@ -22,7 +22,7 @@
 import { Conversation } from 'jami-web-common';
 import { useEffect } from 'react';
 
-import { useAppSelector } from '../../redux/hooks';
+import { useAppSelector } from '../redux/hooks';
 import ConversationListItem from './ConversationListItem';
 
 type ConversationListProps = {
diff --git a/client/src/components/ConversationListItem.js b/client/src/components/ConversationListItem.jsx
similarity index 98%
rename from client/src/components/ConversationListItem.js
rename to client/src/components/ConversationListItem.jsx
index 0bcb0b3..16c1ccc 100644
--- a/client/src/components/ConversationListItem.js
+++ b/client/src/components/ConversationListItem.jsx
@@ -22,9 +22,9 @@
 import Modal from 'react-modal';
 import { useNavigate, useParams } from 'react-router-dom';
 
-import { setRefreshFromSlice } from '../../redux/appSlice';
-import { useAppDispatch } from '../../redux/hooks';
 import authManager from '../AuthManager';
+import { setRefreshFromSlice } from '../redux/appSlice';
+import { useAppDispatch } from '../redux/hooks';
 import ConversationAvatar from './ConversationAvatar';
 import { RemoveContactIcon, VideoCallIcon } from './SvgIcon.tsx';
 import { AudioCallIcon, BlockContactIcon, ContactDetailsIcon, CrossIcon, MessageIcon } from './SvgIcon.tsx';
diff --git a/client/src/components/ConversationsOverviewCard.js b/client/src/components/ConversationsOverviewCard.jsx
similarity index 100%
rename from client/src/components/ConversationsOverviewCard.js
rename to client/src/components/ConversationsOverviewCard.jsx
diff --git a/client/src/components/ListItemLink.js b/client/src/components/ListItemLink.jsx
similarity index 100%
rename from client/src/components/ListItemLink.js
rename to client/src/components/ListItemLink.jsx
diff --git a/client/src/components/Message.js b/client/src/components/Message.jsx
similarity index 100%
rename from client/src/components/Message.js
rename to client/src/components/Message.jsx
diff --git a/client/src/components/MessageList.js b/client/src/components/MessageList.jsx
similarity index 100%
rename from client/src/components/MessageList.js
rename to client/src/components/MessageList.jsx
diff --git a/client/src/components/UsernameChooser.js b/client/src/components/UsernameChooser.jsx
similarity index 100%
rename from client/src/components/UsernameChooser.js
rename to client/src/components/UsernameChooser.jsx
diff --git a/client/src/components/welcome.js b/client/src/components/welcome.jsx
similarity index 90%
rename from client/src/components/welcome.js
rename to client/src/components/welcome.jsx
index ca160a0..39e222b 100644
--- a/client/src/components/welcome.js
+++ b/client/src/components/welcome.jsx
@@ -19,8 +19,6 @@
 import { AnimatePresence, motion } from 'framer-motion';
 import { useState } from 'react';
 
-import JamiLogo from '../../public/jami-logo-icon.svg';
-
 const list = {
   hidden: { opacity: 0 },
   visible: {
@@ -57,7 +55,14 @@
             }}
           >
             <motion.div variants={item}>
-              <JamiLogo size="32px" />
+              <img
+                src="/jami-logo-icon.svg"
+                style={{
+                  width: '32',
+                  height: '32',
+                }}
+                alt="jami n/logo"
+              />
             </motion.div>
             <motion.h1 variants={item}>Welcome to Jami</motion.h1>
             {props.showSetup && (
diff --git a/client/src/i18n.ts b/client/src/i18n.ts
index 63d7e1c..37f9f7f 100644
--- a/client/src/i18n.ts
+++ b/client/src/i18n.ts
@@ -18,11 +18,11 @@
 import i18n from 'i18next';
 import { initReactI18next } from 'react-i18next';
 
-import translationEn from '../public/locale/en/translation.json';
-import translationFr from '../public/locale/fr/translation.json';
+import translationEn from './locale/en/translation.json';
+import translationFr from './locale/fr/translation.json';
 
 i18n.use(initReactI18next).init({
-  debug: process.env.NODE_ENV == 'development',
+  debug: import.meta.env.DEV,
   lng: 'en',
   interpolation: {
     escapeValue: false,
diff --git a/client/src/index.ejs b/client/src/index.ejs
deleted file mode 100644
index d68639a..0000000
--- a/client/src/index.ejs
+++ /dev/null
@@ -1,18 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-
-<head>
-  <meta charset="utf-8" />
-  <link rel="icon" href="/favicon.png" />
-  <link rel="apple-touch-icon" href="/logo192.png" />
-  <meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width" />
-  <meta name="description" content="Jami Web" />
-  <script>const initData=<%- initdata %></script>
-  <title>Jami Web</title>
-</head>
-
-<body>
-  <div id="root"></div>
-</body>
-
-</html>
\ No newline at end of file
diff --git a/client/src/index.tsx b/client/src/index.tsx
index fdd2fe7..83c2ada 100644
--- a/client/src/index.tsx
+++ b/client/src/index.tsx
@@ -15,7 +15,6 @@
  * License along with this program.  If not, see
  * <https://www.gnu.org/licenses/>.
  */
-/// <reference types="webpack/module" />
 'use strict';
 import './index.scss';
 import './i18n';
@@ -23,15 +22,14 @@
 // import config from "../sentry-client.config.json"
 import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
 import { StrictMode } from 'react';
-import { render } from 'react-dom';
 import { createRoot } from 'react-dom/client';
 import { Provider } from 'react-redux';
 import { BrowserRouter as Router } from 'react-router-dom';
 import socketio from 'socket.io-client';
 
-import { store } from '../redux/store';
 import App from './App';
 import { SocketProvider } from './contexts/Socket';
+import { store } from './redux/store';
 
 const queryClient = new QueryClient({
   defaultOptions: {
@@ -61,12 +59,3 @@
     </StrictMode>
   </Provider>
 );
-
-if (import.meta.webpackHot)
-  import.meta.webpackHot.accept('./App', () => {
-    try {
-      render(<App />, container);
-    } catch (e) {
-      location.reload();
-    }
-  });
diff --git a/client/public/locale/en/translation.json b/client/src/locale/en/translation.json
similarity index 100%
rename from client/public/locale/en/translation.json
rename to client/src/locale/en/translation.json
diff --git a/client/public/locale/fr/translation.json b/client/src/locale/fr/translation.json
similarity index 100%
rename from client/public/locale/fr/translation.json
rename to client/src/locale/fr/translation.json
diff --git a/client/src/pages/AccountSettings.tsx b/client/src/pages/AccountSettings.tsx
index 35c0678..fe98277 100644
--- a/client/src/pages/AccountSettings.tsx
+++ b/client/src/pages/AccountSettings.tsx
@@ -20,11 +20,11 @@
 import { useEffect, useState } from 'react';
 import { useParams } from 'react-router';
 
-import { setAccountId, setAccountObject } from '../../redux/appSlice';
-import { useAppDispatch } from '../../redux/hooks';
 import authManager from '../AuthManager';
 import AccountPreferences from '../components/AccountPreferences';
 import Header from '../components/Header';
+import { setAccountId, setAccountObject } from '../redux/appSlice';
+import { useAppDispatch } from '../redux/hooks';
 
 type AccountSettingsProps = {
   accountId?: string;
diff --git a/client/src/pages/AddContactPage.tsx b/client/src/pages/AddContactPage.tsx
index d9c8394..666625c 100644
--- a/client/src/pages/AddContactPage.tsx
+++ b/client/src/pages/AddContactPage.tsx
@@ -19,9 +19,9 @@
 import { Box, Card, CardContent, Container, Fab, Typography } from '@mui/material';
 import { useNavigate, useParams } from 'react-router-dom';
 
-import { setRefreshFromSlice } from '../../redux/appSlice';
-import { useAppDispatch } from '../../redux/hooks';
 import authManager from '../AuthManager';
+import { setRefreshFromSlice } from '../redux/appSlice';
+import { useAppDispatch } from '../redux/hooks';
 
 type AddContactPageProps = {
   accountId: string;
diff --git a/client/src/pages/Messenger.tsx b/client/src/pages/Messenger.tsx
index 1fd2252..6aeb840 100644
--- a/client/src/pages/Messenger.tsx
+++ b/client/src/pages/Messenger.tsx
@@ -20,7 +20,6 @@
 import { useEffect, useState } from 'react';
 import { useParams } from 'react-router';
 
-import { useAppSelector } from '../../redux/hooks';
 import authManager from '../AuthManager';
 //import Sound from 'react-sound';
 import ConversationList from '../components/ConversationList';
@@ -28,6 +27,7 @@
 import Header from '../components/Header';
 import LoadingPage from '../components/Loading';
 import NewContactForm from '../components/NewContactForm';
+import { useAppSelector } from '../redux/hooks';
 import AddContactPage from './AddContactPage';
 
 type MessengerProps = {
diff --git a/client/redux/appSlice.ts b/client/src/redux/appSlice.ts
similarity index 98%
rename from client/redux/appSlice.ts
rename to client/src/redux/appSlice.ts
index 030a45a..d5c01ee 100644
--- a/client/redux/appSlice.ts
+++ b/client/src/redux/appSlice.ts
@@ -19,7 +19,7 @@
 import { Account } from 'jami-web-common';
 
 // Define a type for the slice state
-interface appState {
+export interface appState {
   accountId: string;
   accountObject: Account | null;
   refresh: boolean;
diff --git a/client/redux/hooks.ts b/client/src/redux/hooks.ts
similarity index 100%
rename from client/redux/hooks.ts
rename to client/src/redux/hooks.ts
diff --git a/client/redux/store.ts b/client/src/redux/store.ts
similarity index 100%
rename from client/redux/store.ts
rename to client/src/redux/store.ts
diff --git a/client/tsconfig.json b/client/tsconfig.json
index 9af3397..3a83682 100644
--- a/client/tsconfig.json
+++ b/client/tsconfig.json
@@ -3,8 +3,12 @@
   "compilerOptions": {
     /* Specify what JSX code is generated. */
     "jsx": "react-jsx",
+    /* Specify a set of bundled library declaration files that describe the target runtime environment. */
+    "lib": ["DOM", "DOM.Iterable", "ESNext"],
     /* Specify an output folder for all emitted files. */
-    "outDir": "./dist/"
+    "outDir": "dist",
+    /* Specify type package names to be included without being referenced in a source file. */
+    "types": ["@testing-library/jest-dom", "@types/jest", "node", "vite/client", "vite-plugin-svgr/client"]
   },
-  "include": ["src/"]
+  "include": ["src"]
 }
diff --git a/client/webpack.config.d.ts b/client/vite.config.ts
similarity index 62%
rename from client/webpack.config.d.ts
rename to client/vite.config.ts
index 8deefa8..ee35cfc 100644
--- a/client/webpack.config.d.ts
+++ b/client/vite.config.ts
@@ -15,8 +15,26 @@
  * License along with this program.  If not, see
  * <https://www.gnu.org/licenses/>.
  */
-import { Configuration } from 'webpack';
+import react from '@vitejs/plugin-react';
+import { defineConfig } from 'vite';
+import svgrPlugin from 'vite-plugin-svgr';
 
-declare const config: Configuration;
-
-export default config;
+export default defineConfig({
+  server: {
+    host: '0.0.0.0',
+    port: 3000,
+    proxy: {
+      '^/(api)|(auth)|(setup)': {
+        target: 'http://localhost:3001',
+        secure: false,
+      },
+    },
+  },
+  preview: {
+    port: 8080,
+  },
+  define: {
+    global: {},
+  },
+  plugins: [react(), svgrPlugin()],
+});
diff --git a/client/webpack.config.js b/client/webpack.config.js
deleted file mode 100644
index cf620bc..0000000
--- a/client/webpack.config.js
+++ /dev/null
@@ -1,124 +0,0 @@
-/*
- * Copyright (C) 2022 Savoir-faire Linux Inc.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation; either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public
- * License along with this program.  If not, see
- * <https://www.gnu.org/licenses/>.
- */
-'use strict';
-
-import CopyWebpackPlugin from 'copy-webpack-plugin';
-import dotenv from 'dotenv';
-import HtmlWebpackPlugin from 'html-webpack-plugin';
-import { dirname, resolve } from 'path';
-import { fileURLToPath } from 'url';
-
-const __filename = fileURLToPath(import.meta.url);
-const __dirname = dirname(__filename);
-
-dotenv.config({ path: resolve(__dirname, '..', '.env') });
-
-const mode = process.env.NODE_ENV || 'development';
-
-let entry = [resolve(__dirname, 'src', 'index.tsx')];
-let plugins = [
-  new HtmlWebpackPlugin({
-    template: '!!raw-loader!' + resolve(__dirname, 'src', 'index.ejs'),
-    filename: 'index.ejs',
-  }),
-  new CopyWebpackPlugin({
-    patterns: [
-      {
-        from: resolve(__dirname, 'public'),
-        to: resolve(__dirname, 'dist'),
-      },
-    ],
-  }),
-];
-
-let devtool = undefined;
-let babelLoaderPlugins = ['@babel/plugin-transform-runtime'];
-
-if (mode === 'development') {
-  const webpack = (await import('webpack')).default;
-  const ReactRefreshWebpackPlugin = (await import('@pmmmwh/react-refresh-webpack-plugin')).default;
-  entry = ['webpack-hot-middleware/client', ...entry];
-  plugins = [new webpack.HotModuleReplacementPlugin(), new ReactRefreshWebpackPlugin(), ...plugins];
-  babelLoaderPlugins = [...babelLoaderPlugins, 'react-refresh/babel'];
-  devtool = 'inline-source-map';
-}
-console.log(`Webpack configured for ${mode}`);
-
-export default {
-  entry,
-  output: {
-    path: resolve(__dirname, 'dist'),
-    filename: 'bundle.js',
-    publicPath: '/',
-  },
-  devtool,
-  mode,
-  module: {
-    rules: [
-      {
-        test: /\.[tj]sx?$/,
-        resolve: {
-          fullySpecified: false,
-          extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
-        },
-        exclude: /node_modules/,
-        use: {
-          loader: 'babel-loader',
-          options: {
-            plugins: babelLoaderPlugins,
-            presets: [
-              [
-                '@babel/preset-env',
-                {
-                  useBuiltIns: 'entry',
-                  corejs: { version: '3.10', proposals: true },
-                },
-              ],
-              [
-                '@babel/preset-react',
-                {
-                  runtime: 'automatic',
-                },
-              ],
-              ['@babel/preset-typescript'],
-            ],
-          },
-        },
-      },
-      {
-        test: /\.s[ac]ss$/i,
-        use: ['style-loader', 'css-loader', 'sass-loader'],
-      },
-      {
-        test: /\.svg$/,
-        use: ['@svgr/webpack'],
-      },
-      {
-        // test: /\.tsx?$/,
-        test: /\.(js|jsx|ts|tsx)?$/,
-        exclude: /node_modules/,
-        loader: 'ts-loader',
-      },
-    ],
-  },
-  resolve: {
-    modules: ['node_modules', resolve(__dirname)],
-    extensions: ['.ts', '.tsx', '.js', '.jsx'],
-  },
-  plugins,
-};