Преглед изворни кода

:construction: add redux/router @ vite demo

Jeremy Zheng пре 3 година
родитељ
комит
e78a29c3b9
55 измењених фајлова са 586 додато и 317 уклоњено
  1. 2 0
      dashboard-fluent/.env
  2. 0 10
      dashboard-fluent/.env.orig
  3. 2 9
      dashboard-fluent/.gitignore
  4. 5 0
      dashboard-fluent/README.md
  5. 13 0
      dashboard-fluent/index.html
  6. 17 37
      dashboard-fluent/package.json
  7. BIN
      dashboard-fluent/public/favicon.ico
  8. 0 43
      dashboard-fluent/public/index.html
  9. 1 0
      dashboard-fluent/public/logo.svg
  10. BIN
      dashboard-fluent/public/logo192.png
  11. BIN
      dashboard-fluent/public/logo512.png
  12. 0 25
      dashboard-fluent/public/manifest.json
  13. 0 3
      dashboard-fluent/public/robots.txt
  14. 0 2
      dashboard-fluent/src/App.css
  15. 0 9
      dashboard-fluent/src/App.test.tsx
  16. 19 17
      dashboard-fluent/src/App.tsx
  17. 43 3
      dashboard-fluent/src/Router.tsx
  18. 5 0
      dashboard-fluent/src/components/Loading.tsx
  19. 5 0
      dashboard-fluent/src/hooks.ts
  20. 0 15
      dashboard-fluent/src/index.tsx
  21. 15 0
      dashboard-fluent/src/layouts/anonymous/index.tsx
  22. 15 0
      dashboard-fluent/src/layouts/dashboard/index.tsx
  23. 31 0
      dashboard-fluent/src/locales.ts
  24. 0 3
      dashboard-fluent/src/locales/en-US/index.ts
  25. 0 60
      dashboard-fluent/src/locales/index.ts
  26. 0 11
      dashboard-fluent/src/locales/languages.ts
  27. 0 3
      dashboard-fluent/src/locales/zh-Hans/index.ts
  28. 0 3
      dashboard-fluent/src/locales/zh-Hant/index.ts
  29. 7 0
      dashboard-fluent/src/main.tsx
  30. 0 20
      dashboard-fluent/src/pages/Home.tsx
  31. 0 9
      dashboard-fluent/src/pages/NotFound.tsx
  32. 5 0
      dashboard-fluent/src/pages/forbidden.tsx
  33. 5 0
      dashboard-fluent/src/pages/home.tsx
  34. 5 0
      dashboard-fluent/src/pages/loading.tsx
  35. 5 0
      dashboard-fluent/src/pages/not-found.tsx
  36. 5 0
      dashboard-fluent/src/pages/users/confirm/new.tsx
  37. 8 0
      dashboard-fluent/src/pages/users/confirm/verify.tsx
  38. 5 0
      dashboard-fluent/src/pages/users/forgot-password.tsx
  39. 5 0
      dashboard-fluent/src/pages/users/logs.tsx
  40. 8 0
      dashboard-fluent/src/pages/users/reset-password.tsx
  41. 5 0
      dashboard-fluent/src/pages/users/sign-in.tsx
  42. 5 0
      dashboard-fluent/src/pages/users/sign-up.tsx
  43. 5 0
      dashboard-fluent/src/pages/users/unlock/new.tsx
  44. 8 0
      dashboard-fluent/src/pages/users/unlock/verify.tsx
  45. 0 1
      dashboard-fluent/src/react-app-env.d.ts
  46. 131 0
      dashboard-fluent/src/reducers/current-user.ts
  47. 50 0
      dashboard-fluent/src/reducers/layout.ts
  48. 0 15
      dashboard-fluent/src/reportWebVitals.ts
  49. 99 0
      dashboard-fluent/src/request.ts
  50. 0 5
      dashboard-fluent/src/setupTests.ts
  51. 16 0
      dashboard-fluent/src/store.ts
  52. 1 0
      dashboard-fluent/src/vite-env.d.ts
  53. 9 14
      dashboard-fluent/tsconfig.json
  54. 9 0
      dashboard-fluent/tsconfig.node.json
  55. 17 0
      dashboard-fluent/vite.config.ts

+ 2 - 0
dashboard-fluent/.env

@@ -0,0 +1,2 @@
+VITE_APP_GRPC_HOST=http://localhost:9999
+VITE_APP_ENABLE_LOCAL_TOKEN=true

+ 0 - 10
dashboard-fluent/.env.orig

@@ -1,10 +0,0 @@
-GENERATE_SOURCEMAP=false
-BROWSER=none
-PUBLIC_URL=/my
-HOST=0.0.0.0
-PORT=20080
-
-#REACT_APP_GRPC_HOST=http://127.0.0.1:10012
-REACT_APP_DEFAULT_LOCALE=zh-Hans
-REACT_APP_LANGUAGES=en-US,zh-Hans,zh-Hant
-REACT_APP_API_HOST=https://jeremy.spring.wikipali.org

+ 2 - 9
dashboard-fluent/.gitignore

@@ -1,12 +1,5 @@
 /node_modules/
 /yarn.lock
 
-# production
-/build/
-
-# misc
-.env
-.env.local
-.env.development.local
-.env.test.local
-.env.production.local
+/dist/
+/.env.*

+ 5 - 0
dashboard-fluent/README.md

@@ -0,0 +1,5 @@
+# USAGE
+
+- 开发环境配置文件 `cp .env .env.development.local`
+
+- 修改**port**和**proxy**在: `vite.config.ts`

+ 13 - 0
dashboard-fluent/index.html

@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="icon" type="image/svg+xml" href="/logo.svg" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Palm</title>
+  </head>
+  <body>
+    <div id="root"></div>
+    <script type="module" src="/src/main.tsx"></script>
+  </body>
+</html>

+ 17 - 37
dashboard-fluent/package.json

@@ -1,24 +1,23 @@
 {
   "name": "dashboard",
-  "version": "0.1.0",
   "private": true,
+  "version": "0.0.0",
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "tsc && vite build",
+    "preview": "vite preview"
+  },
   "dependencies": {
-    "@fluentui/react": "^8.98.7",
+    "@fluentui/react": "^8.98.8",
     "@fortawesome/fontawesome-free": "^6.2.0",
     "@reduxjs/toolkit": "^1.8.6",
-    "@testing-library/jest-dom": "^5.14.1",
-    "@testing-library/react": "^13.0.0",
-    "@testing-library/user-event": "^13.2.1",
     "@types/google-protobuf": "^3.15.6",
-    "@types/jest": "^27.0.1",
     "@types/js-cookie": "^3.0.2",
-    "@types/node": "^16.7.13",
-    "@types/react": "^18.0.0",
     "@types/react-big-calendar": "^0.38.2",
     "@types/react-color": "^3.0.6",
     "@types/react-copy-to-clipboard": "^5.0.4",
-    "@types/react-dom": "^18.0.0",
-    "@types/react-pdf": "^5.7.2",
+    "@types/react-pdf": "^5.7.3",
     "@uiw/react-md-editor": "^3.19.1",
     "dayjs": "^1.11.6",
     "dinero.js": "^2.0.0-alpha.8",
@@ -52,36 +51,17 @@
     "react-quill": "^2.0.0",
     "react-redux": "^8.0.4",
     "react-router-dom": "^6.4.2",
-    "react-scripts": "5.0.1",
     "react-sparklines": "^1.7.0",
     "react-syntax-highlighter": "^15.5.0",
+    "redux-thunk": "^2.4.1",
     "timezones-list": "^3.0.1",
-    "typescript": "^4.4.2",
-    "video.js": "^7.20.3",
-    "web-vitals": "^2.1.0"
-  },
-  "scripts": {
-    "start": "react-scripts start",
-    "build": "react-scripts build",
-    "test": "react-scripts test",
-    "eject": "react-scripts eject"
-  },
-  "eslintConfig": {
-    "extends": [
-      "react-app",
-      "react-app/jest"
-    ]
+    "video.js": "^7.20.3"
   },
-  "browserslist": {
-    "production": [
-      ">0.2%",
-      "not dead",
-      "not op_mini all"
-    ],
-    "development": [
-      "last 1 chrome version",
-      "last 1 firefox version",
-      "last 1 safari version"
-    ]
+  "devDependencies": {
+    "@types/react": "^18.0.22",
+    "@types/react-dom": "^18.0.7",
+    "@vitejs/plugin-react": "^2.2.0",
+    "typescript": "^4.6.4",
+    "vite": "^3.2.0"
   }
 }

BIN
dashboard-fluent/public/favicon.ico


+ 0 - 43
dashboard-fluent/public/index.html

@@ -1,43 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-  <head>
-    <meta charset="utf-8" />
-    <link rel="icon" href="%PUBLIC_URL%/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_URL%/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_URL%/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>

+ 1 - 0
dashboard-fluent/public/logo.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

BIN
dashboard-fluent/public/logo192.png


BIN
dashboard-fluent/public/logo512.png


+ 0 - 25
dashboard-fluent/public/manifest.json

@@ -1,25 +0,0 @@
-{
-  "short_name": "React App",
-  "name": "Create React App Sample",
-  "icons": [
-    {
-      "src": "favicon.ico",
-      "sizes": "64x64 32x32 24x24 16x16",
-      "type": "image/x-icon"
-    },
-    {
-      "src": "logo192.png",
-      "type": "image/png",
-      "sizes": "192x192"
-    },
-    {
-      "src": "logo512.png",
-      "type": "image/png",
-      "sizes": "512x512"
-    }
-  ],
-  "start_url": ".",
-  "display": "standalone",
-  "theme_color": "#000000",
-  "background_color": "#ffffff"
-}

+ 0 - 3
dashboard-fluent/public/robots.txt

@@ -1,3 +0,0 @@
-# https://www.robotstxt.org/robotstxt.html
-User-agent: *
-Disallow:

+ 0 - 2
dashboard-fluent/src/App.css

@@ -1,5 +1,3 @@
-@import "~react-quill/dist/quill.snow.css";
-
 body {
   margin: 0;
   padding: 0;

+ 0 - 9
dashboard-fluent/src/App.test.tsx

@@ -1,9 +0,0 @@
-import React from 'react';
-import { render, screen } from '@testing-library/react';
-import App from './App';
-
-test('renders learn react link', () => {
-  render(<App />);
-  const linkElement = screen.getByText(/learn react/i);
-  expect(linkElement).toBeInTheDocument();
-});

+ 19 - 17
dashboard-fluent/src/App.tsx

@@ -1,29 +1,31 @@
-import { BrowserRouter } from "react-router-dom";
+import { Suspense } from "react";
 import { IntlProvider } from "react-intl";
+import { Provider } from "react-redux";
+import { BrowserRouter } from "react-router-dom";
 
+import store from "./store";
+import Loading from "./components/Loading";
 import Router from "./Router";
-import locales, {
-  get as getLocale,
-  DEFAULT as DEFAULT_LOCALE,
-} from "./locales";
+import { get as getLocale } from "./locales";
+
+import "react-quill/dist/quill.snow.css";
 
 import "./App.css";
 
 const lang = getLocale();
-const i18n = locales(lang);
 
-function Widget() {
+const Widget = () => {
   return (
-    <IntlProvider
-      messages={i18n.messages}
-      locale={lang}
-      defaultLocale={DEFAULT_LOCALE}
-    >
-      <BrowserRouter basename={process.env.PUBLIC_URL}>
-        <Router />
-      </BrowserRouter>
-    </IntlProvider>
+    <Provider store={store}>
+      <IntlProvider messages={{}} locale={lang} defaultLocale={"en-US"}>
+        <BrowserRouter basename={import.meta.env.BASE_URL}>
+          <Suspense fallback={<Loading />}>
+            <Router />
+          </Suspense>
+        </BrowserRouter>
+      </IntlProvider>
+    </Provider>
   );
-}
+};
 
 export default Widget;

+ 43 - 3
dashboard-fluent/src/Router.tsx

@@ -1,12 +1,52 @@
+import { lazy } from "react";
 import { Route, Routes } from "react-router-dom";
 
-import Home from "./pages/Home";
-import NotFound from "./pages/NotFound";
+import Anonymous from "./layouts/anonymous";
+import Dashboard from "./layouts/dashboard";
+
+const Home = lazy(() => import("./pages/home"));
+const NotFound = lazy(() => import("./pages/not-found"));
+const Forbidden = lazy(() => import("./pages/forbidden"));
+const Loading = lazy(() => import("./pages/loading"));
+const UsersSignIn = lazy(() => import("./pages/users/sign-in"));
+const UsersSignUp = lazy(() => import("./pages/users/sign-up"));
+const UsersLogs = lazy(() => import("./pages/users/logs"));
+const UsersConfirmNew = lazy(() => import("./pages/users/confirm/new"));
+const UsersConfirmVerify = lazy(() => import("./pages/users/confirm/verify"));
+const UsersUnlockNew = lazy(() => import("./pages/users/unlock/new"));
+const UsersUnlockVerify = lazy(() => import("./pages/users/unlock/verify"));
+const UsersForgotPassword = lazy(() => import("./pages/users/forgot-password"));
+const UsersResetPassword = lazy(() => import("./pages/users/reset-password"));
 
 const Widget = () => {
   return (
     <Routes>
-      {/* PLEASE KEEP THOSE ARE THE LAST TWO ROUTES */}
+      <Route path="anonymous" element={<Anonymous />}>
+        <Route path="users">
+          <Route path="sign-in" element={<UsersSignIn />} />
+          <Route path="sign-up" element={<UsersSignUp />} />
+          <Route path="confirm">
+            <Route path="new" element={<UsersConfirmNew />} />
+            <Route path="verify/:token" element={<UsersConfirmVerify />} />
+          </Route>
+          <Route path="unlock">
+            <Route path="new" element={<UsersUnlockNew />} />
+            <Route path="verify/:token" element={<UsersUnlockVerify />} />
+          </Route>
+          <Route
+            path="reset-password/:token"
+            element={<UsersResetPassword />}
+          />
+          <Route path="forgot-password" element={<UsersForgotPassword />} />
+        </Route>
+      </Route>
+      <Route path="dashboard" element={<Dashboard />}>
+        <Route path="users">
+          <Route path="logs" element={<UsersLogs />} />
+        </Route>
+      </Route>
+      <Route path="loading" element={<Loading />} />
+      <Route path="forbidden" element={<Forbidden />} />
       <Route path="" element={<Home />} />
       <Route path="*" element={<NotFound />} />
     </Routes>

+ 5 - 0
dashboard-fluent/src/components/Loading.tsx

@@ -0,0 +1,5 @@
+const Widget = () => {
+  return <div>loading</div>;
+};
+
+export default Widget;

+ 5 - 0
dashboard-fluent/src/hooks.ts

@@ -0,0 +1,5 @@
+import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
+import type { RootState, AppDispatch } from "./store";
+
+export const useAppDispatch: () => AppDispatch = useDispatch;
+export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

+ 0 - 15
dashboard-fluent/src/index.tsx

@@ -1,15 +0,0 @@
-import React from "react";
-import ReactDOM from "react-dom/client";
-
-import App from "./App";
-import reportWebVitals from "./reportWebVitals";
-
-const root = ReactDOM.createRoot(
-  document.getElementById("root") as HTMLElement
-);
-root.render(<App />);
-
-// If you want to start measuring performance in your app, pass a function
-// to log results (for example: reportWebVitals(console.log))
-// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
-reportWebVitals();

+ 15 - 0
dashboard-fluent/src/layouts/anonymous/index.tsx

@@ -0,0 +1,15 @@
+import { Outlet } from "react-router-dom";
+
+const Widget = () => {
+  return (
+    <div>
+      <div>anonymous header</div>
+      <div>
+        <Outlet />
+      </div>
+      <div>anonymous footer</div>
+    </div>
+  );
+};
+
+export default Widget;

+ 15 - 0
dashboard-fluent/src/layouts/dashboard/index.tsx

@@ -0,0 +1,15 @@
+import { Outlet } from "react-router-dom";
+
+const Widget = () => {
+  return (
+    <div>
+      <div>dashboard header</div>
+      <div>
+        <Outlet />
+      </div>
+      <div>dashboard footer</div>
+    </div>
+  );
+};
+
+export default Widget;

+ 31 - 0
dashboard-fluent/src/locales.ts

@@ -0,0 +1,31 @@
+import Cookies from "js-cookie";
+
+import "moment/locale/zh-cn";
+import "moment/locale/zh-tw";
+import "moment/locale/es";
+import "moment/locale/fr";
+import "moment/locale/ja";
+import "moment/locale/ko";
+
+const KEY = "locale";
+
+export const get = (): string => {
+  return localStorage.getItem(KEY) || Cookies.get(KEY) || "en-US";
+};
+
+export const set = (lang: string, reload: boolean) => {
+  Cookies.set(KEY, lang);
+  localStorage.setItem(KEY, lang);
+  if (reload) {
+    window.location.reload();
+  }
+};
+
+export const remove = () => {
+  Cookies.remove(KEY);
+  localStorage.removeItem(KEY);
+};
+
+interface ILocale {
+  messages: Record<string, string>;
+}

+ 0 - 3
dashboard-fluent/src/locales/en-US/index.ts

@@ -1,3 +0,0 @@
-const items = {};
-
-export default items;

+ 0 - 60
dashboard-fluent/src/locales/index.ts

@@ -1,60 +0,0 @@
-import Cookies from "js-cookie";
-import "moment/locale/zh-cn";
-import "moment/locale/zh-tw";
-import "moment/locale/es";
-import "moment/locale/fr";
-import "moment/locale/ja";
-import "moment/locale/ko";
-
-import languages from "./languages";
-import enUS from "./en-US";
-import zhHans from "./zh-Hans";
-import zhHant from "./zh-Hant";
-
-const KEY = "locale";
-
-export const DEFAULT: string =
-  process.env.REACT_APP_DEFAULT_LOCALE || "zh-Hans";
-export const LANGUAGES: string[] = process.env.REACT_APP_LANGUAGES?.split(
-  ","
-) || ["en-US", "zh-Hans"];
-
-export const get = (): string => {
-  return localStorage.getItem(KEY) || Cookies.get(KEY) || DEFAULT;
-};
-
-export const set = (lang: string, reload: boolean) => {
-  Cookies.set(KEY, lang);
-  localStorage.setItem(KEY, lang);
-  if (reload) {
-    window.location.reload();
-  }
-};
-
-export const remove = () => {
-  Cookies.remove(KEY);
-  localStorage.removeItem(KEY);
-};
-
-interface ILocale {
-  messages: Record<string, string>;
-}
-
-const messages = (lang: string): ILocale => {
-  switch (lang) {
-    case "en-US":
-      return {
-        messages: { ...enUS, ...languages },
-      };
-    case "zh-Hant":
-      return {
-        messages: { ...zhHant, ...languages },
-      };
-    default:
-      return {
-        messages: { ...zhHans, ...languages },
-      };
-  }
-};
-
-export default messages;

+ 0 - 11
dashboard-fluent/src/locales/languages.ts

@@ -1,11 +0,0 @@
-const items = {
-  "languages.en-US": "English",
-  "languages.zh-Hans": "简体中文",
-  "languages.zh-Hant": "繁體中文",
-  "languages.fr": "Français",
-  "languages.es": "Español",
-  "languages.ja": "日本語",
-  "languages.ko": "한국어",
-};
-
-export default items;

+ 0 - 3
dashboard-fluent/src/locales/zh-Hans/index.ts

@@ -1,3 +0,0 @@
-const items = {};
-
-export default items;

+ 0 - 3
dashboard-fluent/src/locales/zh-Hant/index.ts

@@ -1,3 +0,0 @@
-const items = {};
-
-export default items;

+ 7 - 0
dashboard-fluent/src/main.tsx

@@ -0,0 +1,7 @@
+import ReactDOM from "react-dom/client";
+
+import App from "./App";
+
+ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
+  <App />
+);

+ 0 - 20
dashboard-fluent/src/pages/Home.tsx

@@ -1,20 +0,0 @@
-import { PrimaryButton } from "@fluentui/react";
-
-const Widget = () => {
-  return (
-    <div>
-      <h1>home</h1>
-      <div>
-        <PrimaryButton
-          onClick={() => {
-            alert("aaa");
-          }}
-        >
-          Demo
-        </PrimaryButton>
-      </div>
-    </div>
-  );
-};
-
-export default Widget;

+ 0 - 9
dashboard-fluent/src/pages/NotFound.tsx

@@ -1,9 +0,0 @@
-const Widget = () => {
-  return (
-    <div>
-      <h1>not found</h1>
-    </div>
-  );
-};
-
-export default Widget;

+ 5 - 0
dashboard-fluent/src/pages/forbidden.tsx

@@ -0,0 +1,5 @@
+const Widget = () => {
+  return <div>forbidden</div>;
+};
+
+export default Widget;

+ 5 - 0
dashboard-fluent/src/pages/home.tsx

@@ -0,0 +1,5 @@
+const Widget = () => {
+  return <div>home</div>;
+};
+
+export default Widget;

+ 5 - 0
dashboard-fluent/src/pages/loading.tsx

@@ -0,0 +1,5 @@
+const Widget = () => {
+  return <div>loading</div>;
+};
+
+export default Widget;

+ 5 - 0
dashboard-fluent/src/pages/not-found.tsx

@@ -0,0 +1,5 @@
+const Widget = () => {
+  return <div>not found</div>;
+};
+
+export default Widget;

+ 5 - 0
dashboard-fluent/src/pages/users/confirm/new.tsx

@@ -0,0 +1,5 @@
+const Widget = () => {
+  return <div>confirm new</div>;
+};
+
+export default Widget;

+ 8 - 0
dashboard-fluent/src/pages/users/confirm/verify.tsx

@@ -0,0 +1,8 @@
+import { useParams } from "react-router-dom";
+
+const Widget = () => {
+  const { token } = useParams();
+  return <div>confirm verify {token}</div>;
+};
+
+export default Widget;

+ 5 - 0
dashboard-fluent/src/pages/users/forgot-password.tsx

@@ -0,0 +1,5 @@
+const Widget = () => {
+  return <div>forgot password</div>;
+};
+
+export default Widget;

+ 5 - 0
dashboard-fluent/src/pages/users/logs.tsx

@@ -0,0 +1,5 @@
+const Widget = () => {
+  return <div>logs</div>;
+};
+
+export default Widget;

+ 8 - 0
dashboard-fluent/src/pages/users/reset-password.tsx

@@ -0,0 +1,8 @@
+import { useParams } from "react-router-dom";
+
+const Widget = () => {
+  const { token } = useParams();
+  return <div>reset password {token}</div>;
+};
+
+export default Widget;

+ 5 - 0
dashboard-fluent/src/pages/users/sign-in.tsx

@@ -0,0 +1,5 @@
+const Widget = () => {
+  return <div>sign in</div>;
+};
+
+export default Widget;

+ 5 - 0
dashboard-fluent/src/pages/users/sign-up.tsx

@@ -0,0 +1,5 @@
+const Widget = () => {
+  return <div>sign up</div>;
+};
+
+export default Widget;

+ 5 - 0
dashboard-fluent/src/pages/users/unlock/new.tsx

@@ -0,0 +1,5 @@
+const Widget = () => {
+  return <div>unlock new</div>;
+};
+
+export default Widget;

+ 8 - 0
dashboard-fluent/src/pages/users/unlock/verify.tsx

@@ -0,0 +1,8 @@
+import { useParams } from "react-router-dom";
+
+const Widget = () => {
+  const { token } = useParams();
+  return <div>unlock verify {token}</div>;
+};
+
+export default Widget;

+ 0 - 1
dashboard-fluent/src/react-app-env.d.ts

@@ -1 +0,0 @@
-/// <reference types="react-scripts" />

+ 131 - 0
dashboard-fluent/src/reducers/current-user.ts

@@ -0,0 +1,131 @@
+import { createSlice, PayloadAction } from "@reduxjs/toolkit";
+
+import type { RootState } from "../store";
+
+export const ROLE_ROOT = "root";
+export const ROLE_ADMINISTRATOR = "administrator";
+
+export const TO_SIGN_IN = "/anonymous/users/sign-in";
+export const TO_PROFILE = "/dashboard/users/logs";
+
+const KEY = "token";
+export const DURATION = 60 * 60 * 24;
+
+const ENABLE_LOCAL_TOKEN =
+  import.meta.env.VITE_APP_ENABLE_LOCAL_TOKEN === "true";
+
+export const get = (): string | null => {
+  const token = sessionStorage.getItem(KEY);
+  if (token) {
+    return token;
+  }
+  if (ENABLE_LOCAL_TOKEN) {
+    return localStorage.getItem(KEY);
+  }
+  return null;
+};
+
+const set = (token: string) => {
+  sessionStorage.setItem(KEY, token);
+  if (ENABLE_LOCAL_TOKEN) {
+    localStorage.setItem(KEY, token);
+  }
+};
+
+const remove = () => {
+  sessionStorage.removeItem(KEY);
+  localStorage.removeItem(KEY);
+};
+
+export const OPERATION_READ = "read";
+export const OPERATION_WRITE = "write";
+export const OPERATION_CREATE = "create";
+export const OPERATION_UPDATE = "update";
+export const OPERATION_REMOVE = "remove";
+
+export interface IRoleOption {
+  code: string;
+  name: string;
+}
+
+export const user_option_to_string = (it: IUserOption): string =>
+  `${it.nickName}(${it.realName})`;
+
+export const permission2string = (it: IPermission): string =>
+  `${it.resourceType}://${it.resourceId ? it.resourceId : "*"}/${it.operation}`;
+
+export interface IUserOption {
+  id: number;
+  nickName: string;
+  realName: string;
+}
+
+export interface IUserDetails {
+  id: number;
+  email: string;
+  nickName: string;
+  realName: string;
+  lang: string;
+  uid: string;
+  timeZone: string;
+  avatar: string;
+  signInCount: number;
+  currentSignInAt?: Date;
+  currentSignInIp?: string;
+  lastSignInAt?: Date;
+  lastSignInIp?: string;
+  lockedAt?: Date;
+  confirmedAt?: Date;
+  deletedAt?: Date;
+  updatedAt: Date;
+}
+
+export interface IPermission {
+  operation: string;
+  resourceType: string;
+  resourceId?: number;
+}
+
+export interface IUser {
+  id: number;
+  uid: string;
+  nickName: string;
+  realName: string;
+  avatar: string;
+  permissions: IPermission[];
+  roles: string[];
+}
+
+interface IState {
+  payload?: IUser;
+}
+
+const initialState: IState = {};
+
+export const slice = createSlice({
+  name: "current-user",
+  initialState,
+  reducers: {
+    signIn: (state, action: PayloadAction<[IUser, string]>) => {
+      state.payload = action.payload[0];
+      set(action.payload[1]);
+    },
+    signOut: (state) => {
+      state.payload = undefined;
+      remove();
+    },
+  },
+});
+
+export const { signIn, signOut } = slice.actions;
+
+export const permissions = (state: RootState): IPermission[] =>
+  state.currentUser.payload?.permissions || [];
+export const isRoot = (state: RootState): boolean =>
+  state.currentUser.payload?.roles.includes(ROLE_ROOT) || false;
+export const isAdministrator = (state: RootState): boolean =>
+  state.currentUser.payload?.roles.includes(ROLE_ADMINISTRATOR) || false;
+export const currentUser = (state: RootState): IUser | undefined =>
+  state.currentUser.payload;
+
+export default slice.reducer;

+ 50 - 0
dashboard-fluent/src/reducers/layout.ts

@@ -0,0 +1,50 @@
+import { createSlice, PayloadAction } from "@reduxjs/toolkit";
+
+import type { RootState } from "../store";
+
+export interface IAuthor {
+  name: string;
+  email: string;
+}
+
+export interface ISite {
+  logo: string;
+  title: string;
+  subhead: string;
+  keywords: string[];
+  description: string;
+  copyright: string;
+  author: IAuthor;
+}
+
+interface IState {
+  site?: ISite;
+  title?: string;
+}
+
+const initialState: IState = {};
+
+export const slice = createSlice({
+  name: "layout",
+  initialState,
+  reducers: {
+    refresh: (state, action: PayloadAction<ISite>) => {
+      state.site = action.payload;
+    },
+    setTitle: (state, action: PayloadAction<string>) => {
+      state.title = action.payload;
+    },
+  },
+});
+
+export const { refresh, setTitle } = slice.actions;
+
+export const layout = (state: RootState): IState => state.layout;
+
+export const siteInfo = (state: RootState): ISite | undefined =>
+  state.layout.site;
+
+export const pageTitle = (state: RootState): string | undefined =>
+  state.layout.title;
+
+export default slice.reducer;

+ 0 - 15
dashboard-fluent/src/reportWebVitals.ts

@@ -1,15 +0,0 @@
-import { ReportHandler } from 'web-vitals';
-
-const reportWebVitals = (onPerfEntry?: ReportHandler) => {
-  if (onPerfEntry && onPerfEntry instanceof Function) {
-    import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
-      getCLS(onPerfEntry);
-      getFID(onPerfEntry);
-      getFCP(onPerfEntry);
-      getLCP(onPerfEntry);
-      getTTFB(onPerfEntry);
-    });
-  }
-};
-
-export default reportWebVitals;

+ 99 - 0
dashboard-fluent/src/request.ts

@@ -0,0 +1,99 @@
+import { Metadata } from "grpc-web";
+import { get as getLocale } from "./locales";
+
+import { get as getToken } from "./reducers/current-user";
+
+export const backend = (u: string) => `/api${u}`;
+
+export const GRPC_HOST: string =
+  import.meta.env.VITE_APP_GRPC_HOST || "http://localhost:9999";
+
+export const grpc_metadata = (): Metadata => {
+  return {
+    authorization: `Bearer ${getToken()}`,
+    "accept-language": getLocale() || "en-US",
+  };
+};
+
+export const upload = () => {
+  return {
+    Authorization: `Bearer ${getToken()}`,
+  };
+};
+
+export const options = (method: string): RequestInit => {
+  return {
+    credentials: "include",
+    headers: {
+      Authorization: `Bearer ${getToken()}`,
+      "Content-Type": "application/json; charset=utf-8",
+    },
+    mode: "cors",
+    method,
+  };
+};
+
+export const get = async <R>(path: string): Promise<R> => {
+  const response = await fetch(backend(path), options("GET"));
+  const res: R = await response.json();
+  return res;
+};
+
+export const delete_ = async <R>(path: string): Promise<R> => {
+  const response = await fetch(backend(path), options("DELETE"));
+  const res: R = await response.json();
+  return res;
+};
+
+// https://github.github.io/fetch/#options
+export const post = async <Q, R>(path: string, body: Q): Promise<R> => {
+  const data = options("POST");
+  data.body = JSON.stringify(body);
+  const response = await fetch(backend(path), data);
+  const res: R = await response.json();
+  return res;
+};
+
+export const patch = <Request, Response>(
+  path: string,
+  body: Request
+): Promise<Response> => {
+  const data = options("PATCH");
+  data.body = JSON.stringify(body);
+  return fetch(backend(path), data).then((res) => {
+    if (res.status === 200) {
+      return res.json();
+    }
+    throw res.text();
+  });
+};
+
+export const put = <Request, Response>(
+  path: string,
+  body: Request
+): Promise<Response> => {
+  const data = options("PUT");
+  data.body = JSON.stringify(body);
+  return fetch(backend(path), data).then((res) =>
+    res.status === 200
+      ? res.json()
+      : res.json().then((err) => {
+          throw err;
+        })
+  );
+};
+
+export const download = (path: string, name: string) => {
+  const data = options("GET");
+  fetch(backend(path), data)
+    .then((response) => response.blob())
+    .then((blob) => {
+      var url = window.URL.createObjectURL(blob);
+      var a = document.createElement("a");
+      a.href = url;
+      a.download = name;
+      document.body.appendChild(a); // for firefox
+      a.click();
+      a.remove();
+    });
+};

+ 0 - 5
dashboard-fluent/src/setupTests.ts

@@ -1,5 +0,0 @@
-// jest-dom adds custom jest matchers for asserting on DOM nodes.
-// allows you to do things like:
-// expect(element).toHaveTextContent(/react/i)
-// learn more: https://github.com/testing-library/jest-dom
-import '@testing-library/jest-dom';

+ 16 - 0
dashboard-fluent/src/store.ts

@@ -0,0 +1,16 @@
+import { configureStore } from "@reduxjs/toolkit";
+
+import currentUserReducer from "./reducers/current-user";
+import layoutReducer from "./reducers/layout";
+
+const store = configureStore({
+  reducer: {
+    layout: layoutReducer,
+    currentUser: currentUserReducer,
+  },
+});
+
+export type RootState = ReturnType<typeof store.getState>;
+export type AppDispatch = typeof store.dispatch;
+
+export default store;

+ 1 - 0
dashboard-fluent/src/vite-env.d.ts

@@ -0,0 +1 @@
+/// <reference types="vite/client" />

+ 9 - 14
dashboard-fluent/tsconfig.json

@@ -1,26 +1,21 @@
 {
   "compilerOptions": {
-    "target": "es5",
-    "lib": [
-      "dom",
-      "dom.iterable",
-      "esnext"
-    ],
-    "allowJs": true,
+    "target": "ESNext",
+    "useDefineForClassFields": true,
+    "lib": ["DOM", "DOM.Iterable", "ESNext"],
+    "allowJs": false,
     "skipLibCheck": true,
-    "esModuleInterop": true,
+    "esModuleInterop": false,
     "allowSyntheticDefaultImports": true,
     "strict": true,
     "forceConsistentCasingInFileNames": true,
-    "noFallthroughCasesInSwitch": true,
-    "module": "esnext",
-    "moduleResolution": "node",
+    "module": "ESNext",
+    "moduleResolution": "Node",
     "resolveJsonModule": true,
     "isolatedModules": true,
     "noEmit": true,
     "jsx": "react-jsx"
   },
-  "include": [
-    "src"
-  ]
+  "include": ["src"],
+  "references": [{ "path": "./tsconfig.node.json" }]
 }

+ 9 - 0
dashboard-fluent/tsconfig.node.json

@@ -0,0 +1,9 @@
+{
+  "compilerOptions": {
+    "composite": true,
+    "module": "ESNext",
+    "moduleResolution": "Node",
+    "allowSyntheticDefaultImports": true
+  },
+  "include": ["vite.config.ts"]
+}

+ 17 - 0
dashboard-fluent/vite.config.ts

@@ -0,0 +1,17 @@
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
+
+// https://vitejs.dev/config/
+export default defineConfig({
+  plugins: [react()],
+  base: "/my/",
+  server: {
+    port: 3000,
+    proxy: {
+      "/api": {
+        target: "http://localhost:8080/api",
+        changeOrigin: true,
+      },
+    },
+  },
+});