aboutsummaryrefslogtreecommitdiff
path: root/Timeline/ClientApp
diff options
context:
space:
mode:
authorcrupest <crupest@outlook.com>2020-07-13 20:59:52 +0800
committercrupest <crupest@outlook.com>2020-07-13 20:59:52 +0800
commite69190abb09661caa19fa3905a0d8f3b7e72648b (patch)
tree8030e6a2539347ccb12f0a6bbd31a1f390c6a7b7 /Timeline/ClientApp
parent0a7c884be668267003d7666b444f1022c99a7148 (diff)
downloadtimeline-e69190abb09661caa19fa3905a0d8f3b7e72648b.tar.gz
timeline-e69190abb09661caa19fa3905a0d8f3b7e72648b.tar.bz2
timeline-e69190abb09661caa19fa3905a0d8f3b7e72648b.zip
Move front end to a submodule.
Diffstat (limited to 'Timeline/ClientApp')
m---------Timeline/ClientApp0
-rw-r--r--Timeline/ClientApp/.babelrc28
-rw-r--r--Timeline/ClientApp/.editorconfig13
-rw-r--r--Timeline/ClientApp/.eslintignore5
-rw-r--r--Timeline/ClientApp/.eslintrc.js48
-rw-r--r--Timeline/ClientApp/.gitattributes1
-rw-r--r--Timeline/ClientApp/.gitignore30
-rw-r--r--Timeline/ClientApp/.prettierrc.yaml1
-rw-r--r--Timeline/ClientApp/.yarnrc.yml7
-rw-r--r--Timeline/ClientApp/LICENSE21
-rw-r--r--Timeline/ClientApp/package.json105
-rw-r--r--Timeline/ClientApp/public/android-chrome-192x192.pngbin8519 -> 0 bytes
-rw-r--r--Timeline/ClientApp/public/android-chrome-512x512.pngbin23619 -> 0 bytes
-rw-r--r--Timeline/ClientApp/public/apple-touch-icon.pngbin2088 -> 0 bytes
-rw-r--r--Timeline/ClientApp/public/browserconfig.xml9
-rw-r--r--Timeline/ClientApp/public/favicon-16x16.pngbin682 -> 0 bytes
-rw-r--r--Timeline/ClientApp/public/favicon-32x32.pngbin821 -> 0 bytes
-rw-r--r--Timeline/ClientApp/public/favicon.icobin15086 -> 0 bytes
-rw-r--r--Timeline/ClientApp/public/mstile-144x144.pngbin2259 -> 0 bytes
-rw-r--r--Timeline/ClientApp/public/mstile-150x150.pngbin2240 -> 0 bytes
-rw-r--r--Timeline/ClientApp/public/mstile-310x150.pngbin2482 -> 0 bytes
-rw-r--r--Timeline/ClientApp/public/mstile-310x310.pngbin4937 -> 0 bytes
-rw-r--r--Timeline/ClientApp/public/mstile-70x70.pngbin1561 -> 0 bytes
-rw-r--r--Timeline/ClientApp/public/safari-pinned-tab.svg25
-rw-r--r--Timeline/ClientApp/public/site.webmanifest22
-rw-r--r--Timeline/ClientApp/src/app/App.tsx84
-rw-r--r--Timeline/ClientApp/src/app/about/About.tsx172
-rw-r--r--Timeline/ClientApp/src/app/about/about.sass4
-rw-r--r--Timeline/ClientApp/src/app/about/author-avatar.pngbin12038 -> 0 bytes
-rw-r--r--Timeline/ClientApp/src/app/about/github.pngbin4268 -> 0 bytes
-rw-r--r--Timeline/ClientApp/src/app/admin/Admin.tsx78
-rw-r--r--Timeline/ClientApp/src/app/admin/UserAdmin.tsx463
-rw-r--r--Timeline/ClientApp/src/app/common.ts44
-rw-r--r--Timeline/ClientApp/src/app/common/AlertHost.tsx96
-rw-r--r--Timeline/ClientApp/src/app/common/AppBar.tsx107
-rw-r--r--Timeline/ClientApp/src/app/common/FileInput.tsx41
-rw-r--r--Timeline/ClientApp/src/app/common/ImageCropper.tsx306
-rw-r--r--Timeline/ClientApp/src/app/common/LoadingPage.tsx12
-rw-r--r--Timeline/ClientApp/src/app/common/OperationDialog.tsx381
-rw-r--r--Timeline/ClientApp/src/app/common/SearchInput.tsx63
-rw-r--r--Timeline/ClientApp/src/app/common/TimelineLogo.tsx26
-rw-r--r--Timeline/ClientApp/src/app/common/UserTimelineLogo.tsx26
-rw-r--r--Timeline/ClientApp/src/app/common/alert-service.ts61
-rw-r--r--Timeline/ClientApp/src/app/common/alert.sass15
-rw-r--r--Timeline/ClientApp/src/app/common/common.sass33
-rw-r--r--Timeline/ClientApp/src/app/config.ts1
-rw-r--r--Timeline/ClientApp/src/app/data/base64.ts9
-rw-r--r--Timeline/ClientApp/src/app/data/common.ts25
-rw-r--r--Timeline/ClientApp/src/app/data/timeline.ts346
-rw-r--r--Timeline/ClientApp/src/app/data/user.ts224
-rw-r--r--Timeline/ClientApp/src/app/helper.ts35
-rw-r--r--Timeline/ClientApp/src/app/home/Home.tsx155
-rw-r--r--Timeline/ClientApp/src/app/home/TimelineBoard.tsx54
-rw-r--r--Timeline/ClientApp/src/app/home/TimelineBoardAreaWithUser.tsx36
-rw-r--r--Timeline/ClientApp/src/app/home/TimelineBoardAreaWithoutUser.tsx26
-rw-r--r--Timeline/ClientApp/src/app/home/TimelineCreateDialog.tsx60
-rw-r--r--Timeline/ClientApp/src/app/home/home.sass13
-rw-r--r--Timeline/ClientApp/src/app/i18n.ts79
-rw-r--r--Timeline/ClientApp/src/app/index.sass82
-rw-r--r--Timeline/ClientApp/src/app/index.tsx15
-rw-r--r--Timeline/ClientApp/src/app/locales/en/translation.ts194
-rw-r--r--Timeline/ClientApp/src/app/locales/scheme.ts175
-rw-r--r--Timeline/ClientApp/src/app/locales/zh/translation.ts187
-rw-r--r--Timeline/ClientApp/src/app/service-worker.tsx109
-rw-r--r--Timeline/ClientApp/src/app/settings/Settings.tsx253
-rw-r--r--Timeline/ClientApp/src/app/timeline/Timeline.tsx102
-rw-r--r--Timeline/ClientApp/src/app/timeline/TimelineDeleteDialog.tsx59
-rw-r--r--Timeline/ClientApp/src/app/timeline/TimelineInfoCard.tsx109
-rw-r--r--Timeline/ClientApp/src/app/timeline/TimelineItem.tsx181
-rw-r--r--Timeline/ClientApp/src/app/timeline/TimelineMember.tsx196
-rw-r--r--Timeline/ClientApp/src/app/timeline/TimelinePage.tsx39
-rw-r--r--Timeline/ClientApp/src/app/timeline/TimelinePageTemplate.tsx275
-rw-r--r--Timeline/ClientApp/src/app/timeline/TimelinePageTemplateUI.tsx243
-rw-r--r--Timeline/ClientApp/src/app/timeline/TimelinePageUI.tsx22
-rw-r--r--Timeline/ClientApp/src/app/timeline/TimelinePostEdit.tsx234
-rw-r--r--Timeline/ClientApp/src/app/timeline/TimelinePropertyChangeDialog.tsx70
-rw-r--r--Timeline/ClientApp/src/app/timeline/timeline-ui.sass18
-rw-r--r--Timeline/ClientApp/src/app/timeline/timeline.sass125
-rw-r--r--Timeline/ClientApp/src/app/tsconfig.json13
-rw-r--r--Timeline/ClientApp/src/app/type-utilities.ts1
-rw-r--r--Timeline/ClientApp/src/app/typings.d.ts24
-rw-r--r--Timeline/ClientApp/src/app/user/ChangeAvatarDialog.tsx306
-rw-r--r--Timeline/ClientApp/src/app/user/ChangeNicknameDialog.tsx28
-rw-r--r--Timeline/ClientApp/src/app/user/Login.tsx147
-rw-r--r--Timeline/ClientApp/src/app/user/User.tsx79
-rw-r--r--Timeline/ClientApp/src/app/user/UserInfoCard.tsx116
-rw-r--r--Timeline/ClientApp/src/app/user/UserPage.tsx20
-rw-r--r--Timeline/ClientApp/src/app/user/api.ts79
-rw-r--r--Timeline/ClientApp/src/app/user/user-page.sass11
-rw-r--r--Timeline/ClientApp/src/sw/sw.ts28
-rw-r--r--Timeline/ClientApp/src/sw/tsconfig.json12
-rw-r--r--Timeline/ClientApp/src/tsconfig.json17
-rw-r--r--Timeline/ClientApp/webpack.common.js60
-rw-r--r--Timeline/ClientApp/webpack.config.dev.js78
-rw-r--r--Timeline/ClientApp/webpack.config.prod.js75
95 files changed, 0 insertions, 7172 deletions
diff --git a/Timeline/ClientApp b/Timeline/ClientApp
new file mode 160000
+Subproject d8ab637b8ecf81967d01dc566da6648077bd903
diff --git a/Timeline/ClientApp/.babelrc b/Timeline/ClientApp/.babelrc
deleted file mode 100644
index 78d36a3d..00000000
--- a/Timeline/ClientApp/.babelrc
+++ /dev/null
@@ -1,28 +0,0 @@
-{
- "presets": [
- "@babel/env",
- "@babel/preset-react",
- "@babel/preset-typescript"
- ],
- "plugins": [
- "@babel/plugin-syntax-dynamic-import",
- "@babel/plugin-proposal-class-properties",
- "@babel/plugin-proposal-optional-chaining",
- "@babel/plugin-proposal-nullish-coalescing-operator",
- [
- "@babel/plugin-proposal-decorators",
- {
- "decoratorsBeforeExport": true
- }
- ],
- [
- "babel-plugin-transform-builtin-extend",
- {
- "globals": [
- "Error",
- "Array"
- ]
- }
- ]
- ]
-} \ No newline at end of file
diff --git a/Timeline/ClientApp/.editorconfig b/Timeline/ClientApp/.editorconfig
deleted file mode 100644
index 54d8316a..00000000
--- a/Timeline/ClientApp/.editorconfig
+++ /dev/null
@@ -1,13 +0,0 @@
-root = true
-
-[*.ts]
-tab_width = 2
-
-[*.tsx]
-tab_width = 2
-
-[*.css]
-tab_width = 2
-
-[*.sass]
-tab_width = 2
diff --git a/Timeline/ClientApp/.eslintignore b/Timeline/ClientApp/.eslintignore
deleted file mode 100644
index 371b5fcb..00000000
--- a/Timeline/ClientApp/.eslintignore
+++ /dev/null
@@ -1,5 +0,0 @@
-.yarn
-node_modules
-dist
-webpack.*.js
-.eslintrc.js
diff --git a/Timeline/ClientApp/.eslintrc.js b/Timeline/ClientApp/.eslintrc.js
deleted file mode 100644
index 5232e5d8..00000000
--- a/Timeline/ClientApp/.eslintrc.js
+++ /dev/null
@@ -1,48 +0,0 @@
-module.exports = {
- env: {
- browser: true,
- es6: true,
- },
- extends: [
- 'eslint:recommended',
- 'plugin:react/recommended',
- 'plugin:@typescript-eslint/eslint-recommended',
- 'plugin:@typescript-eslint/recommended',
- 'plugin:@typescript-eslint/recommended-requiring-type-checking',
- 'prettier',
- 'prettier/react',
- 'prettier/@typescript-eslint',
- 'plugin:react-hooks/recommended',
- ],
- globals: {
- Atomics: 'readonly',
- SharedArrayBuffer: 'readonly',
- },
- parser: '@typescript-eslint/parser',
- parserOptions: {
- project: ['./src/app/tsconfig.json', './src/sw/tsconfig.json'],
- ecmaFeatures: {
- jsx: true,
- },
- ecmaVersion: 2018,
- sourceType: 'module',
- },
- plugins: ['react', '@typescript-eslint', 'react-hooks'],
- settings: {
- react: {
- version: 'detect',
- },
- },
- rules: {
- 'react/prop-types': 'off',
- '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
- '@typescript-eslint/explicit-function-return-type': [
- 'warn',
- {
- allowExpressions: true,
- allowTypedFunctionExpressions: true,
- allowHigherOrderFunctions: true,
- },
- ],
- },
-};
diff --git a/Timeline/ClientApp/.gitattributes b/Timeline/ClientApp/.gitattributes
deleted file mode 100644
index c1aa21ac..00000000
--- a/Timeline/ClientApp/.gitattributes
+++ /dev/null
@@ -1 +0,0 @@
-.yarn/** linguist-vendored \ No newline at end of file
diff --git a/Timeline/ClientApp/.gitignore b/Timeline/ClientApp/.gitignore
deleted file mode 100644
index a1707cf2..00000000
--- a/Timeline/ClientApp/.gitignore
+++ /dev/null
@@ -1,30 +0,0 @@
-# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
-
-# dependencies
-/node_modules
-
-.yarn/*
-!.yarn/releases
-!.yarn/plugins
-.pnp.*
-
-# testing
-/coverage
-
-# production
-/build
-
-# misc
-.DS_Store
-.env.local
-.env.development.local
-.env.test.local
-.env.production.local
-
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-
-.vscode
-
-/dist \ No newline at end of file
diff --git a/Timeline/ClientApp/.prettierrc.yaml b/Timeline/ClientApp/.prettierrc.yaml
deleted file mode 100644
index e871b731..00000000
--- a/Timeline/ClientApp/.prettierrc.yaml
+++ /dev/null
@@ -1 +0,0 @@
-singleQuote: true
diff --git a/Timeline/ClientApp/.yarnrc.yml b/Timeline/ClientApp/.yarnrc.yml
deleted file mode 100644
index 1fe52728..00000000
--- a/Timeline/ClientApp/.yarnrc.yml
+++ /dev/null
@@ -1,7 +0,0 @@
-plugins:
- - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.js
- spec: "@yarnpkg/plugin-interactive-tools"
- - path: .yarn/plugins/@yarnpkg/plugin-typescript.js
- spec: "@yarnpkg/plugin-typescript"
-
-yarnPath: .yarn/releases/yarn-berry.js
diff --git a/Timeline/ClientApp/LICENSE b/Timeline/ClientApp/LICENSE
deleted file mode 100644
index c5dd66a5..00000000
--- a/Timeline/ClientApp/LICENSE
+++ /dev/null
@@ -1,21 +0,0 @@
-MIT License
-
-Copyright (c) 2020 杨宇千
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
diff --git a/Timeline/ClientApp/package.json b/Timeline/ClientApp/package.json
deleted file mode 100644
index fa9cca09..00000000
--- a/Timeline/ClientApp/package.json
+++ /dev/null
@@ -1,105 +0,0 @@
-{
- "name": "timeline",
- "version": "0.1.0",
- "private": true,
- "homepage": "https://crupest.xyz",
- "dependencies": {
- "axios": "^0.19.2",
- "bootstrap": "^4.5.0",
- "bootstrap-icons": "^1.0.0-alpha4",
- "classnames": "^2.2.6",
- "clsx": "^1.1.1",
- "core-js": "^3.6.5",
- "i18next": "^19.4.5",
- "i18next-browser-languagedetector": "^4.2.0",
- "lodash": "^4.17.15",
- "pepjs": "^0.5.2",
- "react": "^16.13.1",
- "react-dom": "^16.13.1",
- "react-hot-loader": "^4.12.21",
- "react-i18next": "^11.5.0",
- "react-inlinesvg": "^1.2.0",
- "react-responsive": "^8.1.0",
- "react-router": "^5.2.0",
- "react-router-dom": "^5.2.0",
- "reactstrap": "^8.4.1",
- "regenerator-runtime": "^0.13.5",
- "rxjs": "^6.5.5",
- "workbox-precaching": "^5.1.3",
- "workbox-routing": "^5.1.3",
- "workbox-strategies": "^5.1.3",
- "workbox-window": "^5.1.3",
- "xregexp": "^4.3.0"
- },
- "scripts": {
- "start": "webpack-dev-server --config ./webpack.config.dev.js",
- "build": "webpack --config ./webpack.config.prod.js",
- "install-and-start": "yarn && webpack-dev-server --config ./webpack.config.dev.js",
- "lint": "eslint src/ --ext .js --ext .jsx --ext .ts --ext .tsx"
- },
- "browserslist": {
- "production": [
- ">0.2%",
- "not dead",
- "not op_mini all"
- ],
- "development": [
- "last 1 chrome version",
- "last 1 firefox version",
- "last 1 safari version"
- ]
- },
- "devDependencies": {
- "@babel/core": "^7.10.2",
- "@babel/plugin-proposal-class-properties": "^7.10.1",
- "@babel/plugin-proposal-decorators": "^7.10.1",
- "@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.1",
- "@babel/plugin-proposal-optional-chaining": "^7.10.1",
- "@babel/plugin-syntax-dynamic-import": "^7.8.3",
- "@babel/preset-env": "^7.10.2",
- "@babel/preset-react": "^7.10.1",
- "@babel/preset-typescript": "^7.10.1",
- "@hot-loader/react-dom": "^16.13.0",
- "@types/classnames": "^2.2.10",
- "@types/lodash": "^4.14.155",
- "@types/node": "^14.0.11",
- "@types/react": "^16.9.35",
- "@types/react-dom": "^16.9.8",
- "@types/react-responsive": "^8.0.2",
- "@types/react-router": "^5.1.7",
- "@types/react-router-dom": "^5.1.5",
- "@types/reactstrap": "^8.4.2",
- "@types/webpack-env": "^1.15.2",
- "@types/xregexp": "^4.3.0",
- "@typescript-eslint/eslint-plugin": "^3.1.0",
- "@typescript-eslint/parser": "^3.1.0",
- "autoprefixer": "^9.8.0",
- "babel-loader": "^8.1.0",
- "babel-plugin-transform-builtin-extend": "^1.1.2",
- "clean-webpack-plugin": "^3.0.0",
- "copy-webpack-plugin": "^6.0.2",
- "css-loader": "^3.5.3",
- "eslint": "^7.2.0",
- "eslint-config-prettier": "^6.11.0",
- "eslint-plugin-prettier": "^3.1.3",
- "eslint-plugin-react": "^7.20.0",
- "eslint-plugin-react-hooks": "^4.0.4",
- "file-loader": "^6.0.0",
- "fork-ts-checker-webpack-plugin": "^4.1.6",
- "html-webpack-plugin": "^3.2.0",
- "html-webpack-template": "^6.2.0",
- "http-server": "^0.12.3",
- "pnp-webpack-plugin": "^1.6.4",
- "postcss-loader": "^3.0.0",
- "prettier": "^2.0.5",
- "sass": "^1.26.8",
- "sass-loader": "^8.0.2",
- "style-loader": "^1.2.1",
- "typescript": "^3.9.5",
- "url-loader": "^4.1.0",
- "webpack": "^4.43.0",
- "webpack-cli": "^3.3.11",
- "webpack-dev-server": "^3.11.0",
- "workbox-webpack-plugin": "^5.1.3"
- }
-}
diff --git a/Timeline/ClientApp/public/android-chrome-192x192.png b/Timeline/ClientApp/public/android-chrome-192x192.png
deleted file mode 100644
index da9b6b81..00000000
--- a/Timeline/ClientApp/public/android-chrome-192x192.png
+++ /dev/null
Binary files differ
diff --git a/Timeline/ClientApp/public/android-chrome-512x512.png b/Timeline/ClientApp/public/android-chrome-512x512.png
deleted file mode 100644
index fa84e055..00000000
--- a/Timeline/ClientApp/public/android-chrome-512x512.png
+++ /dev/null
Binary files differ
diff --git a/Timeline/ClientApp/public/apple-touch-icon.png b/Timeline/ClientApp/public/apple-touch-icon.png
deleted file mode 100644
index d5a3fb45..00000000
--- a/Timeline/ClientApp/public/apple-touch-icon.png
+++ /dev/null
Binary files differ
diff --git a/Timeline/ClientApp/public/browserconfig.xml b/Timeline/ClientApp/public/browserconfig.xml
deleted file mode 100644
index a47e5a5b..00000000
--- a/Timeline/ClientApp/public/browserconfig.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<browserconfig>
- <msapplication>
- <tile>
- <square150x150logo src="/mstile-150x150.png"/>
- <TileColor>#2d89ef</TileColor>
- </tile>
- </msapplication>
-</browserconfig>
diff --git a/Timeline/ClientApp/public/favicon-16x16.png b/Timeline/ClientApp/public/favicon-16x16.png
deleted file mode 100644
index 6c978995..00000000
--- a/Timeline/ClientApp/public/favicon-16x16.png
+++ /dev/null
Binary files differ
diff --git a/Timeline/ClientApp/public/favicon-32x32.png b/Timeline/ClientApp/public/favicon-32x32.png
deleted file mode 100644
index bbde902f..00000000
--- a/Timeline/ClientApp/public/favicon-32x32.png
+++ /dev/null
Binary files differ
diff --git a/Timeline/ClientApp/public/favicon.ico b/Timeline/ClientApp/public/favicon.ico
deleted file mode 100644
index d4cd3db6..00000000
--- a/Timeline/ClientApp/public/favicon.ico
+++ /dev/null
Binary files differ
diff --git a/Timeline/ClientApp/public/mstile-144x144.png b/Timeline/ClientApp/public/mstile-144x144.png
deleted file mode 100644
index 61eaaf43..00000000
--- a/Timeline/ClientApp/public/mstile-144x144.png
+++ /dev/null
Binary files differ
diff --git a/Timeline/ClientApp/public/mstile-150x150.png b/Timeline/ClientApp/public/mstile-150x150.png
deleted file mode 100644
index 85fa83ee..00000000
--- a/Timeline/ClientApp/public/mstile-150x150.png
+++ /dev/null
Binary files differ
diff --git a/Timeline/ClientApp/public/mstile-310x150.png b/Timeline/ClientApp/public/mstile-310x150.png
deleted file mode 100644
index 41889953..00000000
--- a/Timeline/ClientApp/public/mstile-310x150.png
+++ /dev/null
Binary files differ
diff --git a/Timeline/ClientApp/public/mstile-310x310.png b/Timeline/ClientApp/public/mstile-310x310.png
deleted file mode 100644
index cddce02e..00000000
--- a/Timeline/ClientApp/public/mstile-310x310.png
+++ /dev/null
Binary files differ
diff --git a/Timeline/ClientApp/public/mstile-70x70.png b/Timeline/ClientApp/public/mstile-70x70.png
deleted file mode 100644
index 52f59d43..00000000
--- a/Timeline/ClientApp/public/mstile-70x70.png
+++ /dev/null
Binary files differ
diff --git a/Timeline/ClientApp/public/safari-pinned-tab.svg b/Timeline/ClientApp/public/safari-pinned-tab.svg
deleted file mode 100644
index 8886ebcc..00000000
--- a/Timeline/ClientApp/public/safari-pinned-tab.svg
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" standalone="no"?>
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
- "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
-<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
- width="700.000000pt" height="700.000000pt" viewBox="0 0 700.000000 700.000000"
- preserveAspectRatio="xMidYMid meet">
-<metadata>
-Created by potrace 1.11, written by Peter Selinger 2001-2013
-</metadata>
-<g transform="translate(0.000000,700.000000) scale(0.100000,-0.100000)"
-fill="#000000" stroke="none">
-<path d="M3080 6207 l0 -794 -57 -13 c-148 -33 -321 -102 -503 -202 -250 -136
--519 -398 -687 -669 -122 -196 -224 -463 -264 -694 -24 -139 -32 -418 -16
--560 72 -648 485 -1236 1069 -1523 68 -33 139 -67 158 -75 51 -22 219 -74 263
--82 l37 -7 0 -794 0 -794 420 0 420 0 0 794 0 794 38 7 c128 23 423 146 573
-238 503 309 846 851 916 1447 10 83 10 355 1 441 -15 127 -49 283 -89 399 -38
-111 -36 105 -108 252 -151 310 -366 554 -673 765 -62 43 -260 149 -306 164
--20 6 -41 15 -47 19 -15 11 -183 66 -248 81 l-57 14 0 792 0 793 -420 0 -420
-0 0 -793z m600 -1601 c14 -3 39 -9 55 -12 17 -2 48 -10 70 -16 22 -6 42 -12
-45 -13 21 -4 117 -45 170 -74 277 -145 492 -417 565 -716 24 -100 26 -116 31
--235 12 -327 -107 -619 -344 -847 -342 -329 -846 -405 -1272 -192 -293 147
--488 387 -584 719 -24 81 -31 151 -31 300 1 234 65 432 203 630 57 82 177 201
-263 263 137 97 323 173 474 193 28 3 52 8 54 10 6 5 274 -3 301 -10z"/>
-</g>
-</svg>
diff --git a/Timeline/ClientApp/public/site.webmanifest b/Timeline/ClientApp/public/site.webmanifest
deleted file mode 100644
index 6746fe59..00000000
--- a/Timeline/ClientApp/public/site.webmanifest
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "$schema": "http://json.schemastore.org/web-manifest",
-
- "name": "Timeline",
- "short_name": "Timeline",
- "description": "Record your life in Timeline! Created by crupest.",
- "icons": [
- {
- "src": "/android-chrome-192x192.png",
- "sizes": "192x192",
- "type": "image/png"
- },
- {
- "src": "/android-chrome-512x512.png",
- "sizes": "512x512",
- "type": "image/png"
- }
- ],
- "theme_color": "#ffffff",
- "background_color": "#ffffff",
- "display": "standalone"
-}
diff --git a/Timeline/ClientApp/src/app/App.tsx b/Timeline/ClientApp/src/app/App.tsx
deleted file mode 100644
index 83ea4ea5..00000000
--- a/Timeline/ClientApp/src/app/App.tsx
+++ /dev/null
@@ -1,84 +0,0 @@
-import React from 'react';
-import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
-import { hot } from 'react-hot-loader/root';
-
-import AppBar from './common/AppBar';
-import LoadingPage from './common/LoadingPage';
-import Home from './home/Home';
-import Login from './user/Login';
-import Settings from './settings/Settings';
-import About from './about/About';
-import User from './user/User';
-import TimelinePage from './timeline/TimelinePage';
-import AlertHost from './common/AlertHost';
-
-import { checkUserLoginState, useOptionalUser } from './data/user';
-
-const NoMatch: React.FC = () => {
- return (
- <>
- <AppBar />
- <div style={{ height: 56 }} />
- <div>Ah-oh, 404!</div>
- </>
- );
-};
-
-const LazyAdmin = React.lazy(() =>
- import(/* webpackChunkName: "admin" */ './admin/Admin')
-);
-
-const App: React.FC = () => {
- const user = useOptionalUser();
-
- React.useEffect(() => {
- void checkUserLoginState();
- }, []);
-
- let body;
- if (user === undefined) {
- body = <LoadingPage />;
- } else {
- body = (
- <Router>
- <Switch>
- <Route exact path="/">
- <Home />
- </Route>
- <Route exact path="/login">
- <Login />
- </Route>
- <Route path="/settings">
- <Settings />
- </Route>
- <Route path="/about">
- <About />
- </Route>
- <Route path="/timelines/:name">
- <TimelinePage />
- </Route>
- <Route path="/users/:username">
- <User />
- </Route>
- {user && user.administrator && (
- <Route path="/admin">
- <LazyAdmin user={user} />
- </Route>
- )}
- <Route>
- <NoMatch />
- </Route>
- </Switch>
- </Router>
- );
- }
-
- return (
- <React.Suspense fallback={<LoadingPage />}>
- {body}
- <AlertHost />
- </React.Suspense>
- );
-};
-
-export default hot(App);
diff --git a/Timeline/ClientApp/src/app/about/About.tsx b/Timeline/ClientApp/src/app/about/About.tsx
deleted file mode 100644
index 841ec6fe..00000000
--- a/Timeline/ClientApp/src/app/about/About.tsx
+++ /dev/null
@@ -1,172 +0,0 @@
-import React from 'react';
-import { useTranslation, Trans } from 'react-i18next';
-
-import authorAvatarUrl from './author-avatar.png';
-import githubLogoUrl from './github.png';
-
-import AppBar from '../common/AppBar';
-
-const frontendCredits: {
- name: string;
- url: string;
-}[] = [
- {
- name: 'reactjs',
- url: 'https://reactjs.org',
- },
- {
- name: 'typescript',
- url: 'https://www.typescriptlang.org',
- },
- {
- name: 'bootstrap',
- url: 'https://getbootstrap.com',
- },
- {
- name: 'reactstrap',
- url: 'https://reactstrap.github.io',
- },
- {
- name: 'babeljs',
- url: 'https://babeljs.io',
- },
- {
- name: 'webpack',
- url: 'https://webpack.js.org',
- },
- {
- name: 'sass',
- url: 'https://sass-lang.com',
- },
- {
- name: 'eslint',
- url: 'https://eslint.org',
- },
- {
- name: 'prettier',
- url: 'https://prettier.io',
- },
- {
- name: 'pepjs',
- url: 'https://github.com/jquery/PEP',
- },
- {
- name: 'react-inlinesvg',
- url: 'https://github.com/gilbarbara/react-inlinesvg',
- },
-];
-
-const backendCredits: {
- name: string;
- url: string;
-}[] = [
- {
- name: 'ASP.NET Core',
- url: 'https://dotnet.microsoft.com/learn/aspnet/what-is-aspnet-core',
- },
- { name: 'sqlite', url: 'https://sqlite.org' },
- {
- name: 'ImageSharp',
- url: 'https://github.com/SixLabors/ImageSharp',
- },
-];
-
-const About: React.FC = () => {
- const { t } = useTranslation();
-
- return (
- <>
- <AppBar />
- <div className="mt-appbar px-2 mb-4">
- <div className="container mt-4 py-3 shadow border border-primary rounded bg-light">
- <h4 id="author-info">{t('about.author.title')}</h4>
- <div>
- <div className="d-flex">
- <img
- src={authorAvatarUrl}
- className="align-self-start avatar large rounded-circle"
- />
- <div>
- <p>
- <small>{t('about.author.fullname')}</small>
- <span className="text-primary">杨宇千</span>
- </p>
- <p>
- <small>{t('about.author.nickname')}</small>
- <span className="text-primary">crupest</span>
- </p>
- <p>
- <small>{t('about.author.introduction')}</small>
- {t('about.author.introductionContent')}
- </p>
- </div>
- </div>
- <p>
- <small>{t('about.author.links')}</small>
- <a
- href="https://github.com/crupest"
- target="_blank"
- rel="noopener noreferrer"
- >
- <img
- src={githubLogoUrl}
- className="about-link-icon text-body"
- />
- </a>
- </p>
- </div>
- </div>
- <div className="container mt-4 py-3 shadow border border-primary rounded bg-light">
- <h4>{t('about.site.title')}</h4>
- <p>
- <Trans i18nKey="about.site.content">
- 0<span className="text-primary">1</span>2<b>3</b>4
- <a href="#author-info">5</a>6
- </Trans>
- </p>
- <p>
- <a
- href="https://github.com/crupest/Timeline"
- target="_blank"
- rel="noopener noreferrer"
- >
- {t('about.site.repo')}
- </a>
- </p>
- </div>
- <div className="container mt-4 py-3 shadow border border-primary rounded bg-light">
- <h4>{t('about.credits.title')}</h4>
- <p>{t('about.credits.content')}</p>
- <p>{t('about.credits.frontend')}</p>
- <ul>
- {frontendCredits.map((item, index) => {
- return (
- <li key={index}>
- <a href={item.url} target="_blank" rel="noopener noreferrer">
- {item.name}
- </a>
- </li>
- );
- })}
- <li>...</li>
- </ul>
- <p>{t('about.credits.backend')}</p>
- <ul>
- {backendCredits.map((item, index) => {
- return (
- <li key={index}>
- <a href={item.url} target="_blank" rel="noopener noreferrer">
- {item.name}
- </a>
- </li>
- );
- })}
- <li>...</li>
- </ul>
- </div>
- </div>
- </>
- );
-};
-
-export default About;
diff --git a/Timeline/ClientApp/src/app/about/about.sass b/Timeline/ClientApp/src/app/about/about.sass
deleted file mode 100644
index 3b5840cd..00000000
--- a/Timeline/ClientApp/src/app/about/about.sass
+++ /dev/null
@@ -1,4 +0,0 @@
-.about-link-icon
- @extend .mx-2
- width: 1.2em
- height: 1.2em
diff --git a/Timeline/ClientApp/src/app/about/author-avatar.png b/Timeline/ClientApp/src/app/about/author-avatar.png
deleted file mode 100644
index d890d8d0..00000000
--- a/Timeline/ClientApp/src/app/about/author-avatar.png
+++ /dev/null
Binary files differ
diff --git a/Timeline/ClientApp/src/app/about/github.png b/Timeline/ClientApp/src/app/about/github.png
deleted file mode 100644
index ea6ff545..00000000
--- a/Timeline/ClientApp/src/app/about/github.png
+++ /dev/null
Binary files differ
diff --git a/Timeline/ClientApp/src/app/admin/Admin.tsx b/Timeline/ClientApp/src/app/admin/Admin.tsx
deleted file mode 100644
index 9e7d592f..00000000
--- a/Timeline/ClientApp/src/app/admin/Admin.tsx
+++ /dev/null
@@ -1,78 +0,0 @@
-import React, { Fragment } from 'react';
-import { Nav, NavItem, NavLink } from 'reactstrap';
-import {
- Redirect,
- Route,
- Switch,
- useRouteMatch,
- useHistory
-} from 'react-router';
-import classnames from 'classnames';
-
-import AppBar from '../common/AppBar';
-import UserAdmin from './UserAdmin';
-
-import { UserWithToken } from '../data/user';
-
-interface AdminProps {
- user: UserWithToken;
-}
-
-const Admin: React.FC<AdminProps> = props => {
- const match = useRouteMatch();
- const history = useHistory();
- type TabNames = 'users' | 'more';
-
- const tabName = history.location.pathname.replace(match.path + '/', '');
-
- function toggle(newTab: TabNames): void {
- history.push(`${match.url}/${newTab}`);
- }
-
- const createRoute = (
- name: string,
- body: React.ReactNode
- ): React.ReactNode => {
- return (
- <Route path={`${match.path}/${name}`}>
- <AppBar />
- <div style={{ height: 56 }} className="flex-fix-length" />
- <Nav tabs>
- <NavItem>
- <NavLink
- className={classnames({ active: tabName === 'users' })}
- onClick={() => {
- toggle('users');
- }}
- >
- Users
- </NavLink>
- </NavItem>
- <NavItem>
- <NavLink
- className={classnames({ active: tabName === 'more' })}
- onClick={() => {
- toggle('more');
- }}
- >
- More
- </NavLink>
- </NavItem>
- </Nav>
- {body}
- </Route>
- );
- };
-
- return (
- <Fragment>
- <Switch>
- <Redirect from={match.path} to={`${match.path}/users`} exact />
- {createRoute('users', <UserAdmin user={props.user} />)}
- {createRoute('more', <div>More Page Works</div>)}
- </Switch>
- </Fragment>
- );
-};
-
-export default Admin;
diff --git a/Timeline/ClientApp/src/app/admin/UserAdmin.tsx b/Timeline/ClientApp/src/app/admin/UserAdmin.tsx
deleted file mode 100644
index ba14fc84..00000000
--- a/Timeline/ClientApp/src/app/admin/UserAdmin.tsx
+++ /dev/null
@@ -1,463 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import {
- ListGroupItem,
- Row,
- Col,
- UncontrolledDropdown,
- DropdownToggle,
- DropdownMenu,
- DropdownItem,
- Spinner,
- Button,
-} from 'reactstrap';
-import axios from 'axios';
-
-import OperationDialog from '../common/OperationDialog';
-
-import { User, UserWithToken } from '../data/user';
-import { apiBaseUrl } from '../config';
-
-async function fetchUserList(_token: string): Promise<User[]> {
- const res = await axios.get<User[]>(`${apiBaseUrl}/users`);
- return res.data;
-}
-
-interface CreateUserInfo {
- username: string;
- password: string;
- administrator: boolean;
-}
-
-async function createUser(user: CreateUserInfo, token: string): Promise<User> {
- const res = await axios.post<User>(
- `${apiBaseUrl}/userop/createuser?token=${token}`,
- user
- );
- return res.data;
-}
-
-function deleteUser(username: string, token: string): Promise<void> {
- return axios.delete(`${apiBaseUrl}/users/${username}?token=${token}`);
-}
-
-function changeUsername(
- oldUsername: string,
- newUsername: string,
- token: string
-): Promise<void> {
- return axios.patch(`${apiBaseUrl}/users/${oldUsername}?token=${token}`, {
- username: newUsername,
- });
-}
-
-function changePassword(
- username: string,
- newPassword: string,
- token: string
-): Promise<void> {
- return axios.patch(`${apiBaseUrl}/users/${username}?token=${token}`, {
- password: newPassword,
- });
-}
-
-function changePermission(
- username: string,
- newPermission: boolean,
- token: string
-): Promise<void> {
- return axios.patch(`${apiBaseUrl}/users/${username}?token=${token}`, {
- administrator: newPermission,
- });
-}
-
-const kChangeUsername = 'changeusername';
-const kChangePassword = 'changepassword';
-const kChangePermission = 'changepermission';
-const kDelete = 'delete';
-
-type TChangeUsername = typeof kChangeUsername;
-type TChangePassword = typeof kChangePassword;
-type TChangePermission = typeof kChangePermission;
-type TDelete = typeof kDelete;
-
-type ContextMenuItem =
- | TChangeUsername
- | TChangePassword
- | TChangePermission
- | TDelete;
-
-interface UserCardProps {
- onContextMenu: (item: ContextMenuItem) => void;
- user: User;
-}
-
-const UserItem: React.FC<UserCardProps> = (props) => {
- const user = props.user;
-
- const createClickCallback = (item: ContextMenuItem): (() => void) => {
- return () => {
- props.onContextMenu(item);
- };
- };
-
- return (
- <ListGroupItem className="container">
- <Row className="align-items-center">
- <Col>
- <p className="mb-0 text-primary">{user.username}</p>
- <small
- className={user.administrator ? 'text-danger' : 'text-secondary'}
- >
- {user.administrator ? 'administrator' : 'user'}
- </small>
- </Col>
- <Col className="col-auto">
- <UncontrolledDropdown>
- <DropdownToggle color="warning" className="text-light" caret>
- Manage
- </DropdownToggle>
- <DropdownMenu>
- <DropdownItem onClick={createClickCallback(kChangeUsername)}>
- Change Username
- </DropdownItem>
- <DropdownItem onClick={createClickCallback(kChangePassword)}>
- Change Password
- </DropdownItem>
- <DropdownItem onClick={createClickCallback(kChangePermission)}>
- Change Permission
- </DropdownItem>
- <DropdownItem
- className="text-danger"
- onClick={createClickCallback(kDelete)}
- >
- Delete
- </DropdownItem>
- </DropdownMenu>
- </UncontrolledDropdown>
- </Col>
- </Row>
- </ListGroupItem>
- );
-};
-
-interface DialogProps {
- open: boolean;
- close: () => void;
-}
-
-interface CreateUserDialogProps extends DialogProps {
- process: (user: CreateUserInfo) => Promise<void>;
-}
-
-const CreateUserDialog: React.FC<CreateUserDialogProps> = (props) => {
- return (
- <OperationDialog
- title="Create"
- titleColor="create"
- inputPrompt="You are creating a new user."
- inputScheme={[
- { type: 'text', label: 'Username' },
- { type: 'text', label: 'Password' },
- { type: 'bool', label: 'Administrator' },
- ]}
- onProcess={([username, password, administrator]) =>
- props.process({
- username: username as string,
- password: password as string,
- administrator: administrator as boolean,
- })
- }
- close={props.close}
- open={props.open}
- />
- );
-};
-
-const UsernameLabel: React.FC = (props) => {
- return <span style={{ color: 'blue' }}>{props.children}</span>;
-};
-
-interface UserDeleteDialogProps extends DialogProps {
- username: string;
- process: () => Promise<void>;
-}
-
-const UserDeleteDialog: React.FC<UserDeleteDialogProps> = (props) => {
- return (
- <OperationDialog
- open={props.open}
- close={props.close}
- title="Dangerous"
- titleColor="dangerous"
- inputPrompt={() => (
- <>
- {'You are deleting user '}
- <UsernameLabel>{props.username}</UsernameLabel>
- {' !'}
- </>
- )}
- onProcess={props.process}
- />
- );
-};
-
-interface UserModifyDialogProps<T> extends DialogProps {
- username: string;
- process: (value: T) => Promise<void>;
-}
-
-const UserChangeUsernameDialog: React.FC<UserModifyDialogProps<string>> = (
- props
-) => {
- return (
- <OperationDialog
- open={props.open}
- close={props.close}
- title="Caution"
- titleColor="dangerous"
- inputPrompt={() => (
- <>
- {'You are change the username of user '}
- <UsernameLabel>{props.username}</UsernameLabel>
- {' !'}
- </>
- )}
- inputScheme={[{ type: 'text', label: 'New Username' }]}
- onProcess={([newUsername]) => {
- return props.process(newUsername as string);
- }}
- />
- );
-};
-
-const UserChangePasswordDialog: React.FC<UserModifyDialogProps<string>> = (
- props
-) => {
- return (
- <OperationDialog
- open={props.open}
- close={props.close}
- title="Caution"
- titleColor="dangerous"
- inputPrompt={() => (
- <>
- {'You are change the password of user '}
- <UsernameLabel>{props.username}</UsernameLabel>
- {' !'}
- </>
- )}
- inputScheme={[{ type: 'text', label: 'New Password' }]}
- onProcess={([newPassword]) => {
- return props.process(newPassword as string);
- }}
- />
- );
-};
-
-interface UserChangePermissionDialogProps extends DialogProps {
- username: string;
- newPermission: boolean;
- process: () => Promise<void>;
-}
-
-const UserChangePermissionDialog: React.FC<UserChangePermissionDialogProps> = (
- props
-) => {
- return (
- <OperationDialog
- open={props.open}
- close={props.close}
- title="Caution"
- titleColor="dangerous"
- inputPrompt={() => (
- <>
- {'You are change user '}
- <UsernameLabel>{props.username}</UsernameLabel>
- {' to '}
- <span style={{ color: 'orange' }}>
- {props.newPermission ? 'administrator' : 'normal user'}
- </span>
- {' !'}
- </>
- )}
- onProcess={props.process}
- />
- );
-};
-
-interface UserAdminProps {
- user: UserWithToken;
-}
-
-const UserAdmin: React.FC<UserAdminProps> = (props) => {
- type DialogInfo =
- | null
- | {
- type: 'create';
- }
- | { type: TDelete; username: string }
- | {
- type: TChangeUsername;
- username: string;
- }
- | {
- type: TChangePassword;
- username: string;
- }
- | {
- type: TChangePermission;
- username: string;
- newPermission: boolean;
- };
-
- const [users, setUsers] = useState<User[] | null>(null);
- const [dialog, setDialog] = useState<DialogInfo>(null);
-
- const token = props.user.token;
-
- useEffect(() => {
- let subscribe = true;
- void fetchUserList(props.user.token).then((us) => {
- if (subscribe) {
- setUsers(us);
- }
- });
- return () => {
- subscribe = false;
- };
- }, [props.user]);
-
- let dialogNode: React.ReactNode;
- if (dialog)
- switch (dialog.type) {
- case 'create':
- dialogNode = (
- <CreateUserDialog
- open
- close={() => setDialog(null)}
- process={async (user) => {
- const u = await createUser(user, token);
- setUsers((oldUsers) => [...(oldUsers ?? []), u]);
- }}
- />
- );
- break;
- case 'delete':
- dialogNode = (
- <UserDeleteDialog
- open
- close={() => setDialog(null)}
- username={dialog.username}
- process={async () => {
- await deleteUser(dialog.username, token);
- setUsers((oldUsers) =>
- (oldUsers ?? []).filter((u) => u.username !== dialog.username)
- );
- }}
- />
- );
- break;
- case kChangeUsername:
- dialogNode = (
- <UserChangeUsernameDialog
- open
- close={() => setDialog(null)}
- username={dialog.username}
- process={async (newUsername) => {
- await changeUsername(dialog.username, newUsername, token);
- setUsers((oldUsers) => {
- const users = (oldUsers ?? []).slice();
- const findedUser = users.find(
- (u) => u.username === dialog.username
- );
- if (findedUser) findedUser.username = newUsername;
- return users;
- });
- }}
- />
- );
- break;
- case kChangePassword:
- dialogNode = (
- <UserChangePasswordDialog
- open
- close={() => setDialog(null)}
- username={dialog.username}
- process={async (newPassword) => {
- await changePassword(dialog.username, newPassword, token);
- }}
- />
- );
- break;
- case kChangePermission: {
- const newPermission = dialog.newPermission;
- dialogNode = (
- <UserChangePermissionDialog
- open
- close={() => setDialog(null)}
- username={dialog.username}
- newPermission={newPermission}
- process={async () => {
- await changePermission(dialog.username, newPermission, token);
- setUsers((oldUsers) => {
- const users = (oldUsers ?? []).slice();
- const findedUser = users.find(
- (u) => u.username === dialog.username
- );
- if (findedUser) findedUser.administrator = newPermission;
- return users;
- });
- }}
- />
- );
- break;
- }
- }
-
- if (users) {
- const userComponents = users.map((user) => {
- return (
- <UserItem
- key={user.username}
- user={user}
- onContextMenu={(item) => {
- setDialog(
- item === kChangePermission
- ? {
- type: kChangePermission,
- username: user.username,
- newPermission: !user.administrator,
- }
- : {
- type: item,
- username: user.username,
- }
- );
- }}
- />
- );
- });
-
- return (
- <>
- <Button
- color="success"
- onClick={() =>
- setDialog({
- type: 'create',
- })
- }
- className="align-self-end"
- >
- Create User
- </Button>
- {userComponents}
- {dialogNode}
- </>
- );
- } else {
- return <Spinner />;
- }
-};
-
-export default UserAdmin;
diff --git a/Timeline/ClientApp/src/app/common.ts b/Timeline/ClientApp/src/app/common.ts
deleted file mode 100644
index 56fdbc0e..00000000
--- a/Timeline/ClientApp/src/app/common.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import React from 'react';
-import { Observable, Subject } from 'rxjs';
-
-// This error is thrown when ui goes wrong with bad logic.
-// Such as a variable should not be null, but it does.
-// This error should never occur. If it does, it indicates there is some logic bug in codes.
-export class UiLogicError extends Error {}
-
-export function useEventEmiiter(): [() => Observable<null>, () => void] {
- const ref = React.useRef<Subject<null> | null>(null);
-
- return React.useMemo(() => {
- const getter = (): Subject<null> => {
- if (ref.current == null) {
- ref.current = new Subject<null>();
- }
- return ref.current;
- };
- const trigger = (): void => {
- getter().next(null);
- };
- return [getter, trigger];
- }, []);
-}
-
-export function useValueEventEmiiter<T>(): [
- () => Observable<T>,
- (value: T) => void
-] {
- const ref = React.useRef<Subject<T> | null>(null);
-
- return React.useMemo(() => {
- const getter = (): Subject<T> => {
- if (ref.current == null) {
- ref.current = new Subject<T>();
- }
- return ref.current;
- };
- const trigger = (value: T): void => {
- getter().next(value);
- };
- return [getter, trigger];
- }, []);
-}
diff --git a/Timeline/ClientApp/src/app/common/AlertHost.tsx b/Timeline/ClientApp/src/app/common/AlertHost.tsx
deleted file mode 100644
index 23b6c5f4..00000000
--- a/Timeline/ClientApp/src/app/common/AlertHost.tsx
+++ /dev/null
@@ -1,96 +0,0 @@
-import React, { useCallback } from 'react';
-import { Alert } from 'reactstrap';
-import without from 'lodash/without';
-import concat from 'lodash/concat';
-
-import {
- alertService,
- AlertInfoEx,
- kAlertHostId,
- AlertInfo,
-} from './alert-service';
-import { useTranslation } from 'react-i18next';
-
-interface AutoCloseAlertProps {
- alert: AlertInfo;
- close: () => void;
-}
-
-export const AutoCloseAlert: React.FC<AutoCloseAlertProps> = (props) => {
- const { alert } = props;
- const { dismissTime } = alert;
-
- const { t } = useTranslation();
-
- React.useEffect(() => {
- const tag =
- dismissTime === 'never'
- ? null
- : typeof dismissTime === 'number'
- ? window.setTimeout(props.close, dismissTime)
- : window.setTimeout(props.close, 5000);
- return () => {
- if (tag != null) {
- window.clearTimeout(tag);
- }
- };
- }, [dismissTime, props.close]);
-
- return (
- <Alert className="m-3" color={alert.type ?? 'primary'} toggle={props.close}>
- {(() => {
- const { message } = alert;
- if (typeof message === 'function') {
- const Message = message;
- return <Message />;
- } else if (typeof message === 'object' && message.type === 'i18n') {
- return t(message.key);
- } else return alert.message;
- })()}
- </Alert>
- );
-};
-
-// oh what a bad name!
-interface AlertInfoExEx extends AlertInfoEx {
- close: () => void;
-}
-
-export const AlertHost: React.FC = () => {
- const [alerts, setAlerts] = React.useState<AlertInfoExEx[]>([]);
-
- // react guarantee that state setters are stable, so we don't need to add it to dependency list
-
- const consume = useCallback((alert: AlertInfoEx): void => {
- const alertEx: AlertInfoExEx = {
- ...alert,
- close: () => {
- setAlerts((oldAlerts) => {
- return without(oldAlerts, alertEx);
- });
- },
- };
- setAlerts((oldAlerts) => {
- return concat(oldAlerts, alertEx);
- });
- }, []);
-
- React.useEffect(() => {
- alertService.registerConsumer(consume);
- return () => {
- alertService.unregisterConsumer(consume);
- };
- }, [consume]);
-
- return (
- <div id={kAlertHostId} className="alert-container">
- {alerts.map((alert) => {
- return (
- <AutoCloseAlert key={alert.id} alert={alert} close={alert.close} />
- );
- })}
- </div>
- );
-};
-
-export default AlertHost;
diff --git a/Timeline/ClientApp/src/app/common/AppBar.tsx b/Timeline/ClientApp/src/app/common/AppBar.tsx
deleted file mode 100644
index f75fe08f..00000000
--- a/Timeline/ClientApp/src/app/common/AppBar.tsx
+++ /dev/null
@@ -1,107 +0,0 @@
-import React from 'react';
-import { useHistory, matchPath } from 'react-router';
-import { Link, NavLink } from 'react-router-dom';
-import { Navbar, NavbarToggler, Collapse, Nav, NavItem } from 'reactstrap';
-import { useMediaQuery } from 'react-responsive';
-import { useTranslation } from 'react-i18next';
-
-import { useUser } from '../data/user';
-import { useOptionalVersionedAvatarUrl } from '../user/api';
-
-import TimelineLogo from './TimelineLogo';
-
-const AppBar: React.FC = (_) => {
- const history = useHistory();
- const user = useUser();
- const avatarUrl = useOptionalVersionedAvatarUrl(user?._links?.avatar);
-
- const { t } = useTranslation();
-
- const isUpMd = useMediaQuery({
- minWidth: getComputedStyle(document.documentElement).getPropertyValue(
- '--breakpoint-md'
- ),
- });
-
- const [isMenuOpen, setIsMenuOpen] = React.useState(false);
-
- const toggleMenu = React.useCallback((): void => {
- setIsMenuOpen((oldIsMenuOpen) => !oldIsMenuOpen);
- }, []);
-
- const isAdministrator = user && user.administrator;
-
- const rightArea = (
- <div className="ml-auto mr-2">
- {user != null ? (
- <NavLink to={`/users/${user.username}`}>
- <img
- className="avatar small rounded-circle bg-white"
- src={avatarUrl}
- />
- </NavLink>
- ) : (
- <NavLink className="text-light" to="/login">
- {t('nav.login')}
- </NavLink>
- )}
- </div>
- );
-
- return (
- <Navbar dark className="fixed-top w-100 bg-primary app-bar" expand="md">
- <Link to="/" className="navbar-brand d-flex align-items-center">
- <TimelineLogo style={{ height: '1em' }} />
- Timeline
- </Link>
-
- {isUpMd ? null : rightArea}
-
- <NavbarToggler onClick={toggleMenu} />
- <Collapse isOpen={isMenuOpen} navbar>
- <Nav className="mr-auto" navbar>
- <NavItem
- className={
- matchPath(history.location.pathname, '/settings')
- ? 'active'
- : undefined
- }
- >
- <NavLink className="nav-link" to="/settings">
- {t('nav.settings')}
- </NavLink>
- </NavItem>
-
- <NavItem
- className={
- matchPath(history.location.pathname, '/about')
- ? 'active'
- : undefined
- }
- >
- <NavLink className="nav-link" to="/about">
- {t('nav.about')}
- </NavLink>
- </NavItem>
-
- {isAdministrator && (
- <NavItem
- className={
- matchPath(history.location.pathname, '/admin')
- ? 'active'
- : undefined
- }
- >
- <NavLink className="nav-link" to="/admin">
- Administration
- </NavLink>
- </NavItem>
- )}
- </Nav>
- {isUpMd ? rightArea : null}
- </Collapse>
- </Navbar>
- );
-};
-
-export default AppBar;
diff --git a/Timeline/ClientApp/src/app/common/FileInput.tsx b/Timeline/ClientApp/src/app/common/FileInput.tsx
deleted file mode 100644
index 20da7b71..00000000
--- a/Timeline/ClientApp/src/app/common/FileInput.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import React from 'react';
-import clsx from 'clsx';
-
-import { ExcludeKey } from '../type-utilities';
-
-export interface FileInputProps
- extends ExcludeKey<
- React.InputHTMLAttributes<HTMLInputElement>,
- 'type' | 'id'
- > {
- inputId?: string;
- labelText: string;
- color?: string;
- className?: string;
-}
-
-const FileInput: React.FC<FileInputProps> = props => {
- const { inputId, labelText, color, className, ...otherProps } = props;
-
- const realInputId = React.useMemo<string>(() => {
- if (inputId != null) return inputId;
- return (
- 'file-input-' +
- (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1)
- );
- }, [inputId]);
-
- return (
- <>
- <input className="d-none" type="file" id={realInputId} {...otherProps} />
- <label
- htmlFor={realInputId}
- className={clsx('btn', 'btn-' + (color ?? 'primary'), className)}
- >
- {labelText}
- </label>
- </>
- );
-};
-
-export default FileInput;
diff --git a/Timeline/ClientApp/src/app/common/ImageCropper.tsx b/Timeline/ClientApp/src/app/common/ImageCropper.tsx
deleted file mode 100644
index 7cb8d3cf..00000000
--- a/Timeline/ClientApp/src/app/common/ImageCropper.tsx
+++ /dev/null
@@ -1,306 +0,0 @@
-import * as React from 'react';
-import clsx from 'clsx';
-
-import { UiLogicError } from '../common';
-
-export interface Clip {
- left: number;
- top: number;
- width: number;
-}
-
-interface NormailizedClip extends Clip {
- height: number;
-}
-
-interface ImageInfo {
- width: number;
- height: number;
- landscape: boolean;
- ratio: number;
- maxClipWidth: number;
- maxClipHeight: number;
-}
-
-interface ImageCropperSavedState {
- clip: NormailizedClip;
- x: number;
- y: number;
- pointerId: number;
-}
-
-export interface ImageCropperProps {
- clip: Clip | null;
- imageUrl: string;
- onChange: (clip: Clip) => void;
- imageElementCallback?: (element: HTMLImageElement | null) => void;
- className?: string;
-}
-
-const ImageCropper = (props: ImageCropperProps): React.ReactElement => {
- const { clip, imageUrl, onChange, imageElementCallback, className } = props;
-
- const [oldState, setOldState] = React.useState<ImageCropperSavedState | null>(
- null
- );
- const [imageInfo, setImageInfo] = React.useState<ImageInfo | null>(null);
-
- const normalizeClip = (c: Clip | null | undefined): NormailizedClip => {
- if (c == null) {
- return { left: 0, top: 0, width: 0, height: 0 };
- }
-
- return {
- left: c.left || 0,
- top: c.top || 0,
- width: c.width || 0,
- height: imageInfo != null ? (c.width || 0) / imageInfo.ratio : 0,
- };
- };
-
- const c = normalizeClip(clip);
-
- const imgElementRef = React.useRef<HTMLImageElement | null>(null);
-
- const onImageRef = React.useCallback(
- (e: HTMLImageElement | null) => {
- imgElementRef.current = e;
- if (imageElementCallback != null && e == null) {
- imageElementCallback(null);
- }
- },
- [imageElementCallback]
- );
-
- const onImageLoad = React.useCallback(
- (e: React.SyntheticEvent<HTMLImageElement>) => {
- const img = e.currentTarget;
- const landscape = img.naturalWidth >= img.naturalHeight;
-
- const info = {
- width: img.naturalWidth,
- height: img.naturalHeight,
- landscape,
- ratio: img.naturalHeight / img.naturalWidth,
- maxClipWidth: landscape ? img.naturalHeight / img.naturalWidth : 1,
- maxClipHeight: landscape ? 1 : img.naturalWidth / img.naturalHeight,
- };
- setImageInfo(info);
- onChange({ left: 0, top: 0, width: info.maxClipWidth });
- if (imageElementCallback != null) {
- imageElementCallback(img);
- }
- },
- [onChange, imageElementCallback]
- );
-
- const onPointerDown = React.useCallback(
- (e: React.PointerEvent) => {
- if (oldState != null) return;
- e.currentTarget.setPointerCapture(e.pointerId);
- setOldState({
- x: e.clientX,
- y: e.clientY,
- clip: c,
- pointerId: e.pointerId,
- });
- },
- [oldState, c]
- );
-
- const onPointerUp = React.useCallback(
- (e: React.PointerEvent) => {
- if (oldState == null || oldState.pointerId !== e.pointerId) return;
- e.currentTarget.releasePointerCapture(e.pointerId);
- setOldState(null);
- },
- [oldState]
- );
-
- const onPointerMove = React.useCallback(
- (e: React.PointerEvent) => {
- if (oldState == null) return;
-
- const oldClip = oldState.clip;
-
- const movement = { x: e.clientX - oldState.x, y: e.clientY - oldState.y };
-
- const { current: imgElement } = imgElementRef;
-
- if (imgElement == null) throw new UiLogicError('Image element is null.');
-
- const moveRatio = {
- x: movement.x / imgElement.width,
- y: movement.y / imgElement.height,
- };
-
- const newRatio = {
- x: oldClip.left + moveRatio.x,
- y: oldClip.top + moveRatio.y,
- };
- if (newRatio.x < 0) {
- newRatio.x = 0;
- } else if (newRatio.x > 1 - oldClip.width) {
- newRatio.x = 1 - oldClip.width;
- }
- if (newRatio.y < 0) {
- newRatio.y = 0;
- } else if (newRatio.y > 1 - oldClip.height) {
- newRatio.y = 1 - oldClip.height;
- }
-
- onChange({ left: newRatio.x, top: newRatio.y, width: oldClip.width });
- },
- [oldState, onChange]
- );
-
- const onHandlerPointerMove = React.useCallback(
- (e: React.PointerEvent) => {
- if (oldState == null) return;
-
- const oldClip = oldState.clip;
-
- const movement = { x: e.clientX - oldState.x, y: e.clientY - oldState.y };
-
- const ratio = imageInfo == null ? 1 : imageInfo.ratio;
-
- const { current: imgElement } = imgElementRef;
-
- if (imgElement == null) throw new UiLogicError('Image element is null.');
-
- const moveRatio = {
- x: movement.x / imgElement.width,
- y: movement.x / imgElement.width / ratio,
- };
-
- const newRatio = {
- x: oldClip.width + moveRatio.x,
- y: oldClip.height + moveRatio.y,
- };
-
- const maxRatio = {
- x: Math.min(1 - oldClip.left, newRatio.x),
- y: Math.min(1 - oldClip.top, newRatio.y),
- };
-
- const maxWidthRatio = Math.min(maxRatio.x, maxRatio.y * ratio);
-
- let newWidth;
- if (newRatio.x < 0) {
- newWidth = 0;
- } else if (newRatio.x > maxWidthRatio) {
- newWidth = maxWidthRatio;
- } else {
- newWidth = newRatio.x;
- }
-
- onChange({ left: oldClip.left, top: oldClip.top, width: newWidth });
- },
- [imageInfo, oldState, onChange]
- );
-
- const toPercentage = (n: number): string => `${n}%`;
-
- // fuck!!! I just can't find a better way to implement this in pure css
- const containerStyle: React.CSSProperties = (() => {
- if (imageInfo == null) {
- return { width: '100%', paddingTop: '100%', height: 0 };
- } else {
- if (imageInfo.ratio > 1) {
- return {
- width: toPercentage(100 / imageInfo.ratio),
- paddingTop: '100%',
- height: 0,
- };
- } else {
- return {
- width: '100%',
- paddingTop: toPercentage(100 * imageInfo.ratio),
- height: 0,
- };
- }
- }
- })();
-
- return (
- <div
- className={clsx('image-cropper-container', className)}
- style={containerStyle}
- >
- <img ref={onImageRef} src={imageUrl} onLoad={onImageLoad} alt="to crop" />
- <div className="image-cropper-mask-container">
- <div
- className="image-cropper-mask"
- touch-action="none"
- style={{
- left: toPercentage(c.left * 100),
- top: toPercentage(c.top * 100),
- width: toPercentage(c.width * 100),
- height: toPercentage(c.height * 100),
- }}
- onPointerMove={onPointerMove}
- onPointerDown={onPointerDown}
- onPointerUp={onPointerUp}
- />
- </div>
- <div
- className="image-cropper-handler"
- touch-action="none"
- style={{
- left: `calc(${(c.left + c.width) * 100}% - 15px)`,
- top: `calc(${(c.top + c.height) * 100}% - 15px)`,
- }}
- onPointerMove={onHandlerPointerMove}
- onPointerDown={onPointerDown}
- onPointerUp={onPointerUp}
- />
- </div>
- );
-};
-
-export default ImageCropper;
-
-export function applyClipToImage(
- image: HTMLImageElement,
- clip: Clip,
- mimeType: string
-): Promise<Blob> {
- return new Promise((resolve, reject) => {
- const naturalSize = {
- width: image.naturalWidth,
- height: image.naturalHeight,
- };
- const clipArea = {
- x: naturalSize.width * clip.left,
- y: naturalSize.height * clip.top,
- length: naturalSize.width * clip.width,
- };
-
- const canvas = document.createElement('canvas');
- canvas.width = clipArea.length;
- canvas.height = clipArea.length;
- const context = canvas.getContext('2d');
-
- if (context == null) throw new Error('Failed to create context.');
-
- context.drawImage(
- image,
- clipArea.x,
- clipArea.y,
- clipArea.length,
- clipArea.length,
- 0,
- 0,
- clipArea.length,
- clipArea.length
- );
-
- canvas.toBlob((blob) => {
- if (blob == null) {
- reject(new Error('canvas.toBlob returns null'));
- } else {
- resolve(blob);
- }
- }, mimeType);
- });
-}
diff --git a/Timeline/ClientApp/src/app/common/LoadingPage.tsx b/Timeline/ClientApp/src/app/common/LoadingPage.tsx
deleted file mode 100644
index 81bc74cf..00000000
--- a/Timeline/ClientApp/src/app/common/LoadingPage.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import React from 'react';
-import { Spinner } from 'reactstrap';
-
-const LoadingPage: React.FC = () => {
- return (
- <div className="position-fixed w-100 h-100 d-flex justify-content-center align-items-center">
- <Spinner style={{ height: '2.5rem', width: '2.5rem' }} color="primary" />
- </div>
- );
-};
-
-export default LoadingPage;
diff --git a/Timeline/ClientApp/src/app/common/OperationDialog.tsx b/Timeline/ClientApp/src/app/common/OperationDialog.tsx
deleted file mode 100644
index 30db4053..00000000
--- a/Timeline/ClientApp/src/app/common/OperationDialog.tsx
+++ /dev/null
@@ -1,381 +0,0 @@
-import React, { useState } from 'react';
-import { useTranslation } from 'react-i18next';
-import {
- Spinner,
- Container,
- ModalBody,
- Label,
- Input,
- FormGroup,
- FormFeedback,
- ModalFooter,
- Button,
- Modal,
- ModalHeader,
- FormText,
-} from 'reactstrap';
-
-import { UiLogicError } from '../common';
-
-const DefaultProcessPrompt: React.FC = (_) => {
- return (
- <Container className="justify-content-center align-items-center">
- <Spinner />
- </Container>
- );
-};
-
-interface DefaultErrorPromptProps {
- error?: string;
-}
-
-const DefaultErrorPrompt: React.FC<DefaultErrorPromptProps> = (props) => {
- const { t } = useTranslation();
-
- let result = <p className="text-danger">{t('operationDialog.error')}</p>;
-
- if (props.error != null) {
- result = (
- <>
- {result}
- <p className="text-danger">{props.error}</p>
- </>
- );
- }
-
- return result;
-};
-
-export type OperationInputOptionalError = undefined | null | string;
-
-export interface OperationInputErrorInfo {
- [index: number]: OperationInputOptionalError;
-}
-
-export type OperationInputValidator<TValue> = (
- value: TValue,
- values: (string | boolean)[]
-) => OperationInputOptionalError | OperationInputErrorInfo;
-
-export interface OperationTextInputInfo {
- type: 'text';
- password?: boolean;
- label?: string;
- initValue?: string;
- textFieldProps?: Omit<
- React.InputHTMLAttributes<HTMLInputElement>,
- 'type' | 'value' | 'onChange'
- >;
- helperText?: string;
- validator?: OperationInputValidator<string>;
-}
-
-export interface OperationBoolInputInfo {
- type: 'bool';
- label: string;
- initValue?: boolean;
-}
-
-export interface OperationSelectInputInfoOption {
- value: string;
- label: string;
- icon?: React.ReactElement;
-}
-
-export interface OperationSelectInputInfo {
- type: 'select';
- label: string;
- options: OperationSelectInputInfoOption[];
- initValue?: string;
-}
-
-export type OperationInputInfo =
- | OperationTextInputInfo
- | OperationBoolInputInfo
- | OperationSelectInputInfo;
-
-interface OperationResult {
- type: 'success' | 'failure';
- data: unknown;
-}
-
-interface OperationDialogProps {
- open: boolean;
- close: () => void;
- title: React.ReactNode;
- titleColor?: 'default' | 'dangerous' | 'create' | string;
- onProcess: (inputs: (string | boolean)[]) => Promise<unknown>;
- inputScheme?: OperationInputInfo[];
- inputPrompt?: string | (() => React.ReactNode);
- processPrompt?: () => React.ReactNode;
- successPrompt?: (data: unknown) => React.ReactNode;
- failurePrompt?: (error: unknown) => React.ReactNode;
- onSuccessAndClose?: () => void;
-}
-
-const OperationDialog: React.FC<OperationDialogProps> = (props) => {
- const inputScheme = props.inputScheme ?? [];
-
- const { t } = useTranslation();
-
- type Step = 'input' | 'process' | OperationResult;
- const [step, setStep] = useState<Step>('input');
- const [values, setValues] = useState<(boolean | string)[]>(
- inputScheme.map((i) => {
- if (i.type === 'bool') {
- return i.initValue ?? false;
- } else if (i.type === 'text' || i.type === 'select') {
- return i.initValue ?? '';
- } else {
- throw new UiLogicError('Unknown input scheme.');
- }
- })
- );
- const [inputError, setInputError] = useState<OperationInputErrorInfo>({});
-
- const close = (): void => {
- if (step !== 'process') {
- props.close();
- if (
- typeof step === 'object' &&
- step.type === 'success' &&
- props.onSuccessAndClose
- ) {
- props.onSuccessAndClose();
- }
- } else {
- console.log('Attempt to close modal when processing.');
- }
- };
-
- const onConfirm = (): void => {
- setStep('process');
- props.onProcess(values).then(
- (d: unknown) => {
- setStep({
- type: 'success',
- data: d,
- });
- },
- (e: unknown) => {
- setStep({
- type: 'failure',
- data: e,
- });
- }
- );
- };
-
- let body: React.ReactNode;
- if (step === 'input') {
- let inputPrompt =
- typeof props.inputPrompt === 'function'
- ? props.inputPrompt()
- : props.inputPrompt;
- inputPrompt = <h6>{inputPrompt}</h6>;
-
- const updateValue = (
- index: number,
- newValue: string | boolean
- ): (string | boolean)[] => {
- const oldValues = values;
- const newValues = oldValues.slice();
- newValues[index] = newValue;
- setValues(newValues);
- return newValues;
- };
-
- const testErrorInfo = (errorInfo: OperationInputErrorInfo): boolean => {
- for (let i = 0; i < inputScheme.length; i++) {
- if (inputScheme[i].type === 'text' && errorInfo[i] != null) {
- return true;
- }
- }
- return false;
- };
-
- const calculateError = (
- oldError: OperationInputErrorInfo,
- index: number,
- newError: OperationInputOptionalError | OperationInputErrorInfo
- ): OperationInputErrorInfo => {
- if (newError === undefined) {
- return oldError;
- } else if (newError === null || typeof newError === 'string') {
- return { ...oldError, [index]: newError };
- } else {
- const newInputError: OperationInputErrorInfo = { ...oldError };
- for (const [index, error] of Object.entries(newError)) {
- if (error !== undefined) {
- newInputError[+index] = error as OperationInputOptionalError;
- }
- }
- return newInputError;
- }
- };
-
- const validateAll = (): boolean => {
- let newInputError = inputError;
- for (let i = 0; i < inputScheme.length; i++) {
- const item = inputScheme[i];
- if (item.type === 'text') {
- newInputError = calculateError(
- newInputError,
- i,
- item.validator?.(values[i] as string, values)
- );
- }
- }
- const result = !testErrorInfo(newInputError);
- setInputError(newInputError);
- return result;
- };
-
- body = (
- <>
- <ModalBody>
- {inputPrompt}
- {inputScheme.map((item, index) => {
- const value = values[index];
- const error: string | undefined = ((e) =>
- typeof e === 'string' ? t(e) : undefined)(inputError?.[index]);
-
- if (item.type === 'text') {
- return (
- <FormGroup key={index}>
- {item.label && <Label>{t(item.label)}</Label>}
- <Input
- type={item.password === true ? 'password' : 'text'}
- value={value as string}
- onChange={(e) => {
- const v = e.target.value;
- const newValues = updateValue(index, v);
- setInputError(
- calculateError(
- inputError,
- index,
- item.validator?.(v, newValues)
- )
- );
- }}
- invalid={error != null}
- {...item.textFieldProps}
- />
- {error != null && <FormFeedback>{error}</FormFeedback>}
- {item.helperText && <FormText>{t(item.helperText)}</FormText>}
- </FormGroup>
- );
- } else if (item.type === 'bool') {
- return (
- <FormGroup check key={index}>
- <Input
- type="checkbox"
- value={value as string}
- onChange={(e) => {
- updateValue(
- index,
- (e.target as HTMLInputElement).checked
- );
- }}
- />
- <Label check>{t(item.label)}</Label>
- </FormGroup>
- );
- } else if (item.type === 'select') {
- return (
- <FormGroup key={index}>
- <Label>{t(item.label)}</Label>
- <Input
- type="select"
- value={value as string}
- onChange={(event) => {
- updateValue(index, event.target.value);
- }}
- >
- {item.options.map((option, i) => {
- return (
- <option value={option.value} key={i}>
- {option.icon}
- {t(option.label)}
- </option>
- );
- })}
- </Input>
- </FormGroup>
- );
- }
- })}
- </ModalBody>
- <ModalFooter>
- <Button color="secondary" onClick={close}>
- {t('operationDialog.cancel')}
- </Button>
- <Button
- color="primary"
- disabled={testErrorInfo(inputError)}
- onClick={() => {
- if (validateAll()) {
- onConfirm();
- }
- }}
- >
- {t('operationDialog.confirm')}
- </Button>
- </ModalFooter>
- </>
- );
- } else if (step === 'process') {
- body = (
- <ModalBody>
- {props.processPrompt?.() ?? <DefaultProcessPrompt />}
- </ModalBody>
- );
- } else {
- let content: React.ReactNode;
- const result = step;
- if (result.type === 'success') {
- content =
- props.successPrompt?.(result.data) ?? t('operationDialog.success');
- if (typeof content === 'string')
- content = <p className="text-success">{content}</p>;
- } else {
- content = props.failurePrompt?.(result.data) ?? <DefaultErrorPrompt />;
- if (typeof content === 'string')
- content = <DefaultErrorPrompt error={content} />;
- }
- body = (
- <>
- <ModalBody>{content}</ModalBody>
- <ModalFooter>
- <Button color="primary" onClick={close}>
- {t('operationDialog.ok')}
- </Button>
- </ModalFooter>
- </>
- );
- }
-
- const title = typeof props.title === 'string' ? t(props.title) : props.title;
-
- return (
- <Modal isOpen={props.open} toggle={close}>
- <ModalHeader
- className={
- props.titleColor != null
- ? 'text-' +
- (props.titleColor === 'create'
- ? 'success'
- : props.titleColor === 'dangerous'
- ? 'danger'
- : props.titleColor)
- : undefined
- }
- >
- {title}
- </ModalHeader>
- {body}
- </Modal>
- );
-};
-
-export default OperationDialog;
diff --git a/Timeline/ClientApp/src/app/common/SearchInput.tsx b/Timeline/ClientApp/src/app/common/SearchInput.tsx
deleted file mode 100644
index 50c252fa..00000000
--- a/Timeline/ClientApp/src/app/common/SearchInput.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-import React, { useCallback } from 'react';
-import clsx from 'clsx';
-import { Spinner, Input, Button } from 'reactstrap';
-import { useTranslation } from 'react-i18next';
-
-export interface SearchInputProps {
- value: string;
- onChange: (value: string) => void;
- onButtonClick: () => void;
- className?: string;
- loading?: boolean;
- buttonText?: string;
- placeholder?: string;
- additionalButton?: React.ReactNode;
-}
-
-const SearchInput: React.FC<SearchInputProps> = (props) => {
- const { onChange, onButtonClick } = props;
-
- const { t } = useTranslation();
-
- const onInputChange = useCallback(
- (event: React.ChangeEvent<HTMLInputElement>): void => {
- onChange(event.currentTarget.value);
- },
- [onChange]
- );
-
- const onInputKeyPress = useCallback(
- (event: React.KeyboardEvent<HTMLInputElement>): void => {
- if (event.key === 'Enter') {
- onButtonClick();
- }
- },
- [onButtonClick]
- );
-
- return (
- <div className={clsx('form-inline my-2', props.className)}>
- <Input
- className="mr-sm-2"
- value={props.value}
- onChange={onInputChange}
- onKeyPress={onInputKeyPress}
- placeholder={props.placeholder}
- />
- <div className="mt-2 mt-sm-0 order-sm-last ml-sm-3">
- {props.additionalButton}
- </div>
- <div className="mt-2 mt-sm-0 ml-auto ml-sm-0">
- {props.loading ? (
- <Spinner />
- ) : (
- <Button outline color="primary" onClick={props.onButtonClick}>
- {props.buttonText ?? t('search')}
- </Button>
- )}
- </div>
- </div>
- );
-};
-
-export default SearchInput;
diff --git a/Timeline/ClientApp/src/app/common/TimelineLogo.tsx b/Timeline/ClientApp/src/app/common/TimelineLogo.tsx
deleted file mode 100644
index 8dd9e97b..00000000
--- a/Timeline/ClientApp/src/app/common/TimelineLogo.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import React, { SVGAttributes } from 'react';
-
-export interface TimelineLogoProps extends SVGAttributes<SVGElement> {
- color?: string;
-}
-
-const TimelineLogo: React.FC<TimelineLogoProps> = props => {
- const { color, ...forwardProps } = props;
- const coercedColor = color ?? 'currentcolor';
- return (
- <svg
- className={props.className}
- viewBox="0 0 100 100"
- fill="none"
- strokeWidth="12"
- stroke={coercedColor}
- {...forwardProps}
- >
- <line x1="50" y1="0" x2="50" y2="25" />
- <circle cx="50" cy="50" r="22" />
- <line x1="50" y1="75" x2="50" y2="100" />
- </svg>
- );
-};
-
-export default TimelineLogo;
diff --git a/Timeline/ClientApp/src/app/common/UserTimelineLogo.tsx b/Timeline/ClientApp/src/app/common/UserTimelineLogo.tsx
deleted file mode 100644
index 58a429d8..00000000
--- a/Timeline/ClientApp/src/app/common/UserTimelineLogo.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import React, { SVGAttributes } from 'react';
-
-export interface UserTimelineLogoProps extends SVGAttributes<SVGElement> {
- color?: string;
-}
-
-const UserTimelineLogo: React.FC<UserTimelineLogoProps> = props => {
- const { color, ...forwardProps } = props;
- const coercedColor = color ?? 'currentcolor';
-
- return (
- <svg viewBox="0 0 100 100" {...forwardProps}>
- <g fill="none" stroke={coercedColor} strokeWidth="12">
- <line x1="50" x2="50" y1="0" y2="25" />
- <circle cx="50" cy="50" r="22" />
- <line x1="50" x2="50" y1="75" y2="100" />
- </g>
- <g fill={color}>
- <circle cx="85" cy="75" r="10" />
- <path d="m70,100c0,0 15,-30 30,0.25" />
- </g>
- </svg>
- );
-};
-
-export default UserTimelineLogo;
diff --git a/Timeline/ClientApp/src/app/common/alert-service.ts b/Timeline/ClientApp/src/app/common/alert-service.ts
deleted file mode 100644
index 79eecc82..00000000
--- a/Timeline/ClientApp/src/app/common/alert-service.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-import React from 'react';
-import pull from 'lodash/pull';
-
-export interface AlertInfo {
- type?: 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info';
- message: string | React.FC<unknown> | { type: 'i18n'; key: string };
- dismissTime?: number | 'never';
-}
-
-export interface AlertInfoEx extends AlertInfo {
- id: number;
-}
-
-export type AlertConsumer = (alerts: AlertInfoEx) => void;
-
-export class AlertService {
- private consumers: AlertConsumer[] = [];
- private savedAlerts: AlertInfoEx[] = [];
- private currentId = 1;
-
- private produce(alert: AlertInfoEx): void {
- for (const consumer of this.consumers) {
- consumer(alert);
- }
- }
-
- registerConsumer(consumer: AlertConsumer): void {
- this.consumers.push(consumer);
- if (this.savedAlerts.length !== 0) {
- for (const alert of this.savedAlerts) {
- this.produce(alert);
- }
- this.savedAlerts = [];
- }
- }
-
- unregisterConsumer(consumer: AlertConsumer): void {
- pull(this.consumers, consumer);
- }
-
- push(alert: AlertInfo): void {
- const newAlert: AlertInfoEx = { ...alert, id: this.currentId++ };
- if (this.consumers.length === 0) {
- this.savedAlerts.push(newAlert);
- } else {
- this.produce(newAlert);
- }
- }
-}
-
-export const alertService = new AlertService();
-
-export function pushAlert(alert: AlertInfo): void {
- alertService.push(alert);
-}
-
-export const kAlertHostId = 'alert-host';
-
-export function getAlertHost(): HTMLElement | null {
- return document.getElementById(kAlertHostId);
-}
diff --git a/Timeline/ClientApp/src/app/common/alert.sass b/Timeline/ClientApp/src/app/common/alert.sass
deleted file mode 100644
index 5b6e65c2..00000000
--- a/Timeline/ClientApp/src/app/common/alert.sass
+++ /dev/null
@@ -1,15 +0,0 @@
-.alert-container
- position: fixed
- z-index: $zindex-popover
-
-@include media-breakpoint-up(sm)
- .alert-container
- bottom: 0
- right: 0
-
-@include media-breakpoint-down(sm)
- .alert-container
- bottom: 0
- right: 0
- left: 0
- text-align: center
diff --git a/Timeline/ClientApp/src/app/common/common.sass b/Timeline/ClientApp/src/app/common/common.sass
deleted file mode 100644
index 15d34d7c..00000000
--- a/Timeline/ClientApp/src/app/common/common.sass
+++ /dev/null
@@ -1,33 +0,0 @@
-.image-cropper-container
- position: relative
- box-sizing: border-box
- user-select: none
-
-.image-cropper-container img
- position: absolute
- left: 0
- top: 0
- width: 100%
- height: 100%
-
-.image-cropper-mask-container
- position: absolute
- left: 0
- top: 0
- right: 0
- bottom: 0
- overflow: hidden
-
-.image-cropper-mask
- position: absolute
- box-shadow: 0 0 0 10000px rgba(255, 255, 255, 80%)
- touch-action: none
-
-.image-cropper-handler
- position: absolute
- width: 26px
- height: 26px
- border: black solid 2px
- border-radius: 50%
- background: white
- touch-action: none
diff --git a/Timeline/ClientApp/src/app/config.ts b/Timeline/ClientApp/src/app/config.ts
deleted file mode 100644
index 081a2cf6..00000000
--- a/Timeline/ClientApp/src/app/config.ts
+++ /dev/null
@@ -1 +0,0 @@
-export const apiBaseUrl = '/api';
diff --git a/Timeline/ClientApp/src/app/data/base64.ts b/Timeline/ClientApp/src/app/data/base64.ts
deleted file mode 100644
index 7f7c6fcc..00000000
--- a/Timeline/ClientApp/src/app/data/base64.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-export function base64(blob: Blob): Promise<string> {
- return new Promise<string>(resolve => {
- const reader = new FileReader();
- reader.onload = function() {
- resolve((reader.result as string).replace(/^data:.+;base64,/, ''));
- };
- reader.readAsDataURL(blob);
- });
-}
diff --git a/Timeline/ClientApp/src/app/data/common.ts b/Timeline/ClientApp/src/app/data/common.ts
deleted file mode 100644
index 1cdf93f1..00000000
--- a/Timeline/ClientApp/src/app/data/common.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { AxiosError } from 'axios';
-
-export function extractStatusCode(error: AxiosError): number | null {
- const code = error.response && error.response.status;
- if (typeof code === 'number') {
- return code;
- } else {
- return null;
- }
-}
-
-export interface CommonErrorResponse {
- code: number;
- message: string;
-}
-
-export function extractErrorCode(error: AxiosError): number | null {
- const { response } = error as AxiosError<CommonErrorResponse>;
- const code = response && response.data && response.data.code;
- if (typeof code === 'number') {
- return code;
- } else {
- return null;
- }
-}
diff --git a/Timeline/ClientApp/src/app/data/timeline.ts b/Timeline/ClientApp/src/app/data/timeline.ts
deleted file mode 100644
index a8875701..00000000
--- a/Timeline/ClientApp/src/app/data/timeline.ts
+++ /dev/null
@@ -1,346 +0,0 @@
-import axios from 'axios';
-import XRegExp from 'xregexp';
-
-import { base64 } from './base64';
-import { apiBaseUrl } from '../config';
-import { User, UserAuthInfo, getCurrentUser, UserWithToken } from './user';
-import { UiLogicError } from '../common';
-
-export const kTimelineVisibilities = ['Public', 'Register', 'Private'] as const;
-
-export type TimelineVisibility = typeof kTimelineVisibilities[number];
-
-export const timelineVisibilityTooltipTranslationMap: Record<
- TimelineVisibility,
- string
-> = {
- Public: 'timeline.visibilityTooltip.public',
- Register: 'timeline.visibilityTooltip.register',
- Private: 'timeline.visibilityTooltip.private',
-};
-
-export interface TimelineInfo {
- uniqueId: string;
- name: string;
- description: string;
- owner: User;
- visibility: TimelineVisibility;
- members: User[];
- _links: {
- posts: string;
- };
-}
-
-export interface TimelinePostTextContent {
- type: 'text';
- text: string;
-}
-
-export interface TimelinePostImageContent {
- type: 'image';
- url: string;
-}
-
-export type TimelinePostContent =
- | TimelinePostTextContent
- | TimelinePostImageContent;
-
-export interface TimelinePostInfo {
- id: number;
- content: TimelinePostContent;
- time: Date;
- author: User;
-}
-
-export interface CreatePostRequestTextContent {
- type: 'text';
- text: string;
-}
-
-export interface CreatePostRequestImageContent {
- type: 'image';
- data: Blob;
-}
-
-export type CreatePostRequestContent =
- | CreatePostRequestTextContent
- | CreatePostRequestImageContent;
-
-export interface CreatePostRequest {
- content: CreatePostRequestContent;
- time?: Date;
-}
-
-// TODO: Remove in the future
-export interface TimelineChangePropertyRequest {
- visibility?: TimelineVisibility;
- description?: string;
-}
-
-export interface PersonalTimelineChangePropertyRequest {
- visibility?: TimelineVisibility;
- description?: string;
-}
-
-export interface OrdinaryTimelineChangePropertyRequest {
- // not supported by server now
- // name?: string;
- visibility?: TimelineVisibility;
- description?: string;
-}
-
-//-------------------- begin: internal model --------------------
-
-interface RawTimelinePostTextContent {
- type: 'text';
- text: string;
-}
-
-interface RawTimelinePostImageContent {
- type: 'image';
- url: string;
-}
-
-type RawTimelinePostContent =
- | RawTimelinePostTextContent
- | RawTimelinePostImageContent;
-
-interface RawTimelinePostInfo {
- id: number;
- content: RawTimelinePostContent;
- time: string;
- author: User;
-}
-
-interface RawCreatePostRequestTextContent {
- type: 'text';
- text: string;
-}
-
-interface RawCreatePostRequestImageContent {
- type: 'text';
- data: string;
-}
-
-type RawCreatePostRequestContent =
- | RawCreatePostRequestTextContent
- | RawCreatePostRequestImageContent;
-
-interface RawCreatePostRequest {
- content: RawCreatePostRequestContent;
- time?: string;
-}
-
-//-------------------- end: internal model --------------------
-
-function processRawTimelinePostInfo(
- raw: RawTimelinePostInfo,
- token?: string
-): TimelinePostInfo {
- return {
- ...raw,
- content: (() => {
- if (raw.content.type === 'image' && token != null) {
- return {
- ...raw.content,
- url: raw.content.url + '?token=' + token,
- };
- }
- return raw.content;
- })(),
- time: new Date(raw.time),
- };
-}
-
-type TimelineUrlResolver = (name: string) => string;
-
-export class TimelineServiceTemplate<
- TTimeline extends TimelineInfo,
- TChangePropertyRequest
-> {
- private checkUser(): UserWithToken {
- const user = getCurrentUser();
- if (user == null) {
- throw new UiLogicError('You must login to perform the operation.');
- }
- return user;
- }
-
- constructor(private urlResolver: TimelineUrlResolver) {}
-
- changeProperty(
- name: string,
- req: TChangePropertyRequest
- ): Promise<TTimeline> {
- const user = this.checkUser();
-
- return axios
- .patch<TTimeline>(`${this.urlResolver(name)}?token=${user.token}`, req)
- .then((res) => res.data);
- }
-
- fetch(name: string): Promise<TTimeline> {
- return axios
- .get<TTimeline>(`${this.urlResolver(name)}`)
- .then((res) => res.data);
- }
-
- fetchPosts(name: string): Promise<TimelinePostInfo[]> {
- const token = getCurrentUser()?.token;
- return axios
- .get<RawTimelinePostInfo[]>(
- token == null
- ? `${this.urlResolver(name)}/posts`
- : `${this.urlResolver(name)}/posts?token=${token}`
- )
- .then((res) => res.data.map((p) => processRawTimelinePostInfo(p, token)));
- }
-
- createPost(
- name: string,
- request: CreatePostRequest
- ): Promise<TimelinePostInfo> {
- const user = this.checkUser();
-
- const rawReq: Promise<RawCreatePostRequest> = new Promise<
- RawCreatePostRequestContent
- >((resolve) => {
- if (request.content.type === 'image') {
- void base64(request.content.data).then((d) =>
- resolve({
- ...request.content,
- data: d,
- } as RawCreatePostRequestImageContent)
- );
- } else {
- resolve(request.content);
- }
- }).then((content) => {
- const rawReq: RawCreatePostRequest = {
- content,
- };
- if (request.time != null) {
- rawReq.time = request.time.toISOString();
- }
- return rawReq;
- });
-
- return rawReq
- .then((req) =>
- axios.post<RawTimelinePostInfo>(
- `${this.urlResolver(name)}/posts?token=${user.token}`,
- req
- )
- )
- .then((res) => processRawTimelinePostInfo(res.data, user.token));
- }
-
- deletePost(name: string, id: number): Promise<void> {
- const user = this.checkUser();
-
- return axios.delete(
- `${this.urlResolver(name)}/posts/${id}?token=${user.token}`
- );
- }
-
- addMember(name: string, username: string): Promise<void> {
- const user = this.checkUser();
-
- return axios.put(
- `${this.urlResolver(name)}/members/${username}?token=${user.token}`
- );
- }
-
- removeMember(name: string, username: string): Promise<void> {
- const user = this.checkUser();
-
- return axios.delete(
- `${this.urlResolver(name)}/members/${username}?token=${user.token}`
- );
- }
-
- isMemberOf(username: string, timeline: TTimeline): boolean {
- return timeline.members.findIndex((m) => m.username == username) >= 0;
- }
-
- hasReadPermission(
- user: UserAuthInfo | null | undefined,
- timeline: TTimeline
- ): boolean {
- if (user != null && user.administrator) return true;
-
- const { visibility } = timeline;
- if (visibility === 'Public') {
- return true;
- } else if (visibility === 'Register') {
- if (user != null) return true;
- } else if (visibility === 'Private') {
- if (user != null && this.isMemberOf(user.username, timeline)) {
- return true;
- }
- }
- return false;
- }
-
- hasPostPermission(
- user: UserAuthInfo | null | undefined,
- timeline: TTimeline
- ): boolean {
- if (user != null && user.administrator) return true;
-
- return (
- user != null &&
- (timeline.owner.username === user.username ||
- this.isMemberOf(user.username, timeline))
- );
- }
-
- hasManagePermission(
- user: UserAuthInfo | null | undefined,
- timeline: TTimeline
- ): boolean {
- if (user != null && user.administrator) return true;
-
- return user != null && user.username == timeline.owner.username;
- }
-
- hasModifyPostPermission(
- user: UserAuthInfo | null | undefined,
- timeline: TTimeline,
- post: TimelinePostInfo
- ): boolean {
- if (user != null && user.administrator) return true;
-
- return (
- user != null &&
- (user.username === timeline.owner.username ||
- user.username === post.author.username)
- );
- }
-}
-
-export type PersonalTimelineService = TimelineServiceTemplate<
- TimelineInfo,
- PersonalTimelineChangePropertyRequest
->;
-
-export const personalTimelineService: PersonalTimelineService = new TimelineServiceTemplate<
- TimelineInfo,
- PersonalTimelineChangePropertyRequest
->((name) => `${apiBaseUrl}/timelines/@${name}`);
-
-export type OrdinaryTimelineService = TimelineServiceTemplate<
- TimelineInfo,
- OrdinaryTimelineChangePropertyRequest
->;
-
-export const ordinaryTimelineService: OrdinaryTimelineService = new TimelineServiceTemplate<
- TimelineInfo,
- TimelineChangePropertyRequest
->((name) => `${apiBaseUrl}/timelines/${name}`);
-
-const timelineNameReg = XRegExp('^[-_\\p{L}]*$', 'u');
-
-export function validateTimelineName(name: string): boolean {
- return timelineNameReg.test(name);
-}
diff --git a/Timeline/ClientApp/src/app/data/user.ts b/Timeline/ClientApp/src/app/data/user.ts
deleted file mode 100644
index 8f787478..00000000
--- a/Timeline/ClientApp/src/app/data/user.ts
+++ /dev/null
@@ -1,224 +0,0 @@
-import axios, { AxiosError } from 'axios';
-import { useState, useEffect } from 'react';
-import { BehaviorSubject, Observable } from 'rxjs';
-
-import { apiBaseUrl } from '../config';
-import { extractErrorCode } from './common';
-import { pushAlert } from '../common/alert-service';
-import { i18nPromise } from '../i18n';
-import { UiLogicError } from '../common';
-
-export interface UserAuthInfo {
- username: string;
- administrator: boolean;
-}
-
-export interface User {
- username: string;
- administrator: boolean;
- nickname: string;
- _links: {
- avatar: string;
- timeline: string;
- };
-}
-
-export interface UserWithToken extends User {
- token: string;
-}
-
-interface CreateTokenRequest {
- username: string;
- password: string;
-}
-
-interface CreateTokenResponse {
- token: string;
- user: User;
-}
-
-interface VerifyTokenRequest {
- token: string;
-}
-
-interface VerifyTokenResponse {
- user: User;
-}
-
-export type LoginCredentials = CreateTokenRequest;
-
-const userSubject = new BehaviorSubject<UserWithToken | null | undefined>(
- undefined
-);
-
-export const user$: Observable<UserWithToken | null | undefined> = userSubject;
-
-export function getCurrentUser(): UserWithToken | null | undefined {
- return userSubject.value;
-}
-
-const kCreateTokenUrl = '/token/create';
-const kVerifyTokenUrl = '/token/verify';
-const createTokenUrl = apiBaseUrl + kCreateTokenUrl;
-const verifyTokenUrl = apiBaseUrl + kVerifyTokenUrl;
-
-function verifyToken(token: string): Promise<User> {
- return axios
- .post<VerifyTokenResponse>(verifyTokenUrl, {
- token: token,
- } as VerifyTokenRequest)
- .then((res) => res.data.user);
-}
-
-const TOKEN_STORAGE_KEY = 'token';
-
-export function checkUserLoginState(): Promise<UserWithToken | null> {
- if (getCurrentUser() !== undefined)
- throw new UiLogicError("Already checked user. Can't check twice.");
-
- const savedToken = window.localStorage.getItem(TOKEN_STORAGE_KEY);
- if (savedToken) {
- return verifyToken(savedToken)
- .then(
- (u) => {
- const user: UserWithToken = {
- ...u,
- token: savedToken,
- };
- void i18nPromise.then((t) => {
- pushAlert({
- type: 'success',
- message: t('user.welcomeBack'),
- });
- });
- return user;
- },
- (e: AxiosError) => {
- if (e.response != null) {
- window.localStorage.removeItem(TOKEN_STORAGE_KEY);
- void i18nPromise.then((t) => {
- pushAlert({
- type: 'danger',
- message: t('user.verifyTokenFailed'),
- });
- });
- } else {
- void i18nPromise.then((t) => {
- pushAlert({
- type: 'danger',
- message: t('user.verifyTokenFailedNetwork'),
- });
- });
- }
-
- return null;
- }
- )
- .then((u) => {
- userSubject.next(u);
- return u;
- });
- }
- userSubject.next(null);
- return Promise.resolve(null);
-}
-
-export class BadCredentialError {
- constructor(public innerError: Error) {}
-
- message = 'login.badCredential';
-}
-
-export function userLogin(
- credentials: LoginCredentials,
- rememberMe: boolean
-): Promise<UserWithToken> {
- if (getCurrentUser()) {
- throw new UiLogicError('Already login.');
- }
- return axios
- .post<CreateTokenResponse>(createTokenUrl, { ...credentials, expire: 30 })
- .catch((e: AxiosError) => {
- if (extractErrorCode(e) === 11010101) {
- throw new BadCredentialError(e);
- }
- throw e;
- })
- .then((res) => {
- const body = res.data;
- const token = body.token;
- if (rememberMe) {
- window.localStorage.setItem(TOKEN_STORAGE_KEY, token);
- }
- const user = {
- ...body.user,
- token,
- };
- userSubject.next(user);
- return user;
- });
-}
-
-export function userLogout(): void {
- if (getCurrentUser() === undefined) {
- throw new UiLogicError('Please check user first.');
- }
- if (getCurrentUser() === null) {
- throw new UiLogicError('No login.');
- }
- window.localStorage.removeItem(TOKEN_STORAGE_KEY);
- userSubject.next(null);
-}
-
-export function useOptionalUser(): UserWithToken | null | undefined {
- const [user, setUser] = useState<UserWithToken | null | undefined>(
- userSubject.value
- );
- useEffect(() => {
- const sub = user$.subscribe((u) => setUser(u));
- return () => {
- sub.unsubscribe();
- };
- });
- return user;
-}
-
-export function useUser(): UserWithToken | null {
- const [user, setUser] = useState<UserWithToken | null>(() => {
- const initUser = userSubject.value;
- if (initUser === undefined) {
- throw new UiLogicError(
- "This is a logic error in user module. Current user can't be undefined in useUser."
- );
- }
- return initUser;
- });
- useEffect(() => {
- const sub = user$.subscribe((u) => {
- if (u === undefined) {
- throw new UiLogicError(
- "This is a logic error in user module. User emitted can't be undefined later."
- );
- }
- setUser(u);
- });
- return () => {
- sub.unsubscribe();
- };
- });
- return user;
-}
-
-export function useUserLoggedIn(): UserWithToken {
- const user = useUser();
- if (user == null) {
- throw new UiLogicError('You assert user has logged in but actually not.');
- }
- return user;
-}
-
-export function fetchUser(username: string): Promise<User> {
- return axios
- .get<User>(`${apiBaseUrl}/users/${username}`)
- .then((res) => res.data);
-}
diff --git a/Timeline/ClientApp/src/app/helper.ts b/Timeline/ClientApp/src/app/helper.ts
deleted file mode 100644
index b48829b5..00000000
--- a/Timeline/ClientApp/src/app/helper.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-//copied from https://stackoverflow.com/questions/5999118/how-can-i-add-or-update-a-query-string-parameter
-export function updateQueryString(
- key: string,
- value?: string | null,
- url?: string
-): string {
- if (!url) url = window.location.href;
- const re = new RegExp('([?&])' + key + '=.*?(&|#|$)(.*)', 'gi');
- let hash;
-
- if (re.test(url)) {
- if (typeof value !== 'undefined' && value !== null) {
- return url.replace(re, '$1' + key + '=' + value + '$2$3');
- } else {
- hash = url.split('#');
- url = hash[0].replace(re, '$1$3').replace(/(&|\?)$/, '');
- if (typeof hash[1] !== 'undefined' && hash[1] !== null) {
- url += '#' + hash[1];
- }
- return url;
- }
- } else {
- if (typeof value !== 'undefined' && value !== null) {
- const separator = url.includes('?') ? '&' : '?';
- hash = url.split('#');
- url = hash[0] + separator + key + '=' + value;
- if (typeof hash[1] !== 'undefined' && hash[1] !== null) {
- url += '#' + hash[1];
- }
- return url;
- } else {
- return url;
- }
- }
-}
diff --git a/Timeline/ClientApp/src/app/home/Home.tsx b/Timeline/ClientApp/src/app/home/Home.tsx
deleted file mode 100644
index 775a1a87..00000000
--- a/Timeline/ClientApp/src/app/home/Home.tsx
+++ /dev/null
@@ -1,155 +0,0 @@
-import React from 'react';
-import { useHistory } from 'react-router';
-import { Row, Container, Button, Col } from 'reactstrap';
-import { useTranslation } from 'react-i18next';
-import axios from 'axios';
-
-import { apiBaseUrl } from '../config';
-import { useUser } from '../data/user';
-import { TimelineInfo } from '../data/timeline';
-
-import AppBar from '../common/AppBar';
-import SearchInput from '../common/SearchInput';
-import TimelineBoardAreaWithoutUser from './TimelineBoardAreaWithoutUser';
-import TimelineBoardAreaWithUser from './TimelineBoardAreaWithUser';
-import TimelineCreateDialog from './TimelineCreateDialog';
-
-const Home: React.FC = (_) => {
- const history = useHistory();
-
- const { t } = useTranslation();
-
- const user = useUser();
-
- const [navText, setNavText] = React.useState<string>('');
-
- const [publicTimelines, setPublicTimelines] = React.useState<
- TimelineInfo[] | undefined
- >(undefined);
- const [ownTimelines, setOwnTimelines] = React.useState<
- TimelineInfo[] | undefined
- >(undefined);
- const [joinTimelines, setJoinTimelines] = React.useState<
- TimelineInfo[] | undefined
- >(undefined);
-
- React.useEffect(() => {
- let subscribe = true;
- if (user == null) {
- setOwnTimelines(undefined);
- setJoinTimelines(undefined);
- void axios
- .get<TimelineInfo[]>(`${apiBaseUrl}/timelines?visibility=public`)
- .then((res) => {
- if (subscribe) {
- setPublicTimelines(res.data);
- }
- });
- } else {
- setPublicTimelines(undefined);
- void axios
- .get<TimelineInfo[]>(
- `${apiBaseUrl}/timelines?relate=${user.username}&relateType=own`
- )
- .then((res) => {
- if (subscribe) {
- setOwnTimelines(res.data);
- }
- });
- void axios
- .get<TimelineInfo[]>(
- `${apiBaseUrl}/timelines?relate=${user.username}&relateType=join`
- )
- .then((res) => {
- if (subscribe) {
- setJoinTimelines(res.data);
- }
- });
- }
- return () => {
- subscribe = false;
- };
- }, [user]);
-
- const [dialog, setDialog] = React.useState<'create' | null>(null);
-
- const goto = React.useCallback((): void => {
- if (navText === '') {
- history.push('users/crupest');
- } else if (navText.startsWith('@')) {
- history.push(`users/${navText.slice(1)}`);
- } else {
- history.push(`timelines/${navText}`);
- }
- }, [navText, history]);
-
- const openCreateDialog = React.useCallback(() => {
- setDialog('create');
- }, []);
-
- const closeDialog = React.useCallback(() => {
- setDialog(null);
- }, []);
-
- return (
- <>
- <AppBar />
- <Container fluid style={{ marginTop: '56px' }}>
- <Row>
- <Col>
- <SearchInput
- className="justify-content-center"
- value={navText}
- onChange={setNavText}
- onButtonClick={goto}
- buttonText={t('home.go')}
- placeholder="@crupest"
- additionalButton={
- user != null && (
- <Button color="success" outline onClick={openCreateDialog}>
- {t('home.createButton')}
- </Button>
- )
- }
- />
- </Col>
- </Row>
- {(() => {
- if (user == null) {
- return (
- <TimelineBoardAreaWithoutUser publicTimelines={publicTimelines} />
- );
- } else {
- return (
- <TimelineBoardAreaWithUser
- ownTimelines={ownTimelines}
- joinTimelines={joinTimelines}
- />
- );
- }
- })()}
- </Container>
- <footer className="text-right">
- <a
- className="mx-3 text-muted"
- href="http://beian.miit.gov.cn/"
- target="_blank"
- rel="noopener noreferrer"
- >
- <small>鄂ICP备18030913号-1</small>
- </a>
- <a
- className="mx-3 text-muted"
- href="http://www.beian.gov.cn/"
- target="_blank"
- rel="noopener noreferrer"
- >
- <small className="white-space-no-wrap">公安备案 42112102000124</small>
- </a>
- </footer>
- {dialog === 'create' && <TimelineCreateDialog open close={closeDialog} />}
- </>
- );
-};
-
-export default Home;
diff --git a/Timeline/ClientApp/src/app/home/TimelineBoard.tsx b/Timeline/ClientApp/src/app/home/TimelineBoard.tsx
deleted file mode 100644
index ca77ab43..00000000
--- a/Timeline/ClientApp/src/app/home/TimelineBoard.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-import React from 'react';
-import clsx from 'clsx';
-import { Link } from 'react-router-dom';
-import { Spinner } from 'reactstrap';
-
-import { TimelineInfo } from '../data/timeline';
-
-import TimelineLogo from '../common/TimelineLogo';
-import UserTimelineLogo from '../common/UserTimelineLogo';
-
-export interface TimelineBoardProps {
- title?: string;
- timelines?: TimelineInfo[];
- className?: string;
-}
-
-const TimelineBoard: React.FC<TimelineBoardProps> = props => {
- const { title, timelines, className } = props;
-
- return (
- <div className={clsx('timeline-board', className)}>
- {title != null && <h3 className="text-center">{title}</h3>}
- {(() => {
- if (timelines == null) {
- return (
- <div className="d-flex flex-grow-1 justify-content-center align-items-center">
- <Spinner color="primary" />
- </div>
- );
- } else {
- return timelines.map(timeline => {
- const { name } = timeline;
- const isPersonal = name.startsWith('@');
- const url = isPersonal
- ? `/users/${timeline.owner.username}`
- : `/timelines/${name}`;
- return (
- <div key={name} className="timeline-board-item">
- {isPersonal ? (
- <UserTimelineLogo className="icon" />
- ) : (
- <TimelineLogo className="icon" />
- )}
- <Link to={url}>{name}</Link>
- </div>
- );
- });
- }
- })()}
- </div>
- );
-};
-
-export default TimelineBoard;
diff --git a/Timeline/ClientApp/src/app/home/TimelineBoardAreaWithUser.tsx b/Timeline/ClientApp/src/app/home/TimelineBoardAreaWithUser.tsx
deleted file mode 100644
index ea1c6beb..00000000
--- a/Timeline/ClientApp/src/app/home/TimelineBoardAreaWithUser.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import React from 'react';
-import { Row, Col } from 'reactstrap';
-import { useTranslation } from 'react-i18next';
-
-import TimelineBoard from './TimelineBoard';
-import { TimelineInfo } from '../data/timeline';
-
-interface TimelineBoardAreaWithUserProps {
- ownTimelines?: TimelineInfo[];
- joinTimelines?: TimelineInfo[];
-}
-
-const TimelineBoardAreaWithUser: React.FC<TimelineBoardAreaWithUserProps> = (
- props
-) => {
- const { t } = useTranslation();
-
- return (
- <Row className="my-2 justify-content-center">
- <Col sm="6" lg="5" className="py-2">
- <TimelineBoard
- title={t('home.ownTimeline')}
- timelines={props.ownTimelines}
- />
- </Col>
- <Col sm="6" lg="5" className="py-2">
- <TimelineBoard
- title={t('home.joinTimeline')}
- timelines={props.joinTimelines}
- />
- </Col>
- </Row>
- );
-};
-
-export default TimelineBoardAreaWithUser;
diff --git a/Timeline/ClientApp/src/app/home/TimelineBoardAreaWithoutUser.tsx b/Timeline/ClientApp/src/app/home/TimelineBoardAreaWithoutUser.tsx
deleted file mode 100644
index c79558b6..00000000
--- a/Timeline/ClientApp/src/app/home/TimelineBoardAreaWithoutUser.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import React from 'react';
-import { Row, Col } from 'reactstrap';
-
-import { TimelineInfo } from '../data/timeline';
-
-import TimelineBoard from './TimelineBoard';
-
-interface TimelineBoardAreaWithoutUserProps {
- publicTimelines?: TimelineInfo[];
-}
-
-const TimelineBoardAreaWithoutUser: React.FC<TimelineBoardAreaWithoutUserProps> = (
- props
-) => {
- const { publicTimelines } = props;
-
- return (
- <Row className="my-2 justify-content-center">
- <Col sm="8" lg="6">
- <TimelineBoard timelines={publicTimelines} />
- </Col>
- </Row>
- );
-};
-
-export default TimelineBoardAreaWithoutUser;
diff --git a/Timeline/ClientApp/src/app/home/TimelineCreateDialog.tsx b/Timeline/ClientApp/src/app/home/TimelineCreateDialog.tsx
deleted file mode 100644
index 925c6c76..00000000
--- a/Timeline/ClientApp/src/app/home/TimelineCreateDialog.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-import React from 'react';
-import { useHistory } from 'react-router';
-import axios from 'axios';
-
-import { apiBaseUrl } from '../config';
-import { useUserLoggedIn } from '../data/user';
-import { validateTimelineName } from '../data/timeline';
-
-import OperationDialog from '../common/OperationDialog';
-
-interface TimelineCreateDialogProps {
- open: boolean;
- close: () => void;
-}
-
-const TimelineCreateDialog: React.FC<TimelineCreateDialogProps> = (props) => {
- const history = useHistory();
- const user = useUserLoggedIn();
-
- let nameSaved: string;
-
- return (
- <OperationDialog
- open={props.open}
- close={props.close}
- titleColor="success"
- title="home.createDialog.title"
- inputScheme={[
- {
- type: 'text',
- label: 'home.createDialog.name',
- helperText: 'home.createDialog.nameFormat',
- validator: (name) => {
- if (name.length === 0) {
- return 'home.createDialog.noEmpty';
- } else if (name.length > 26) {
- return 'home.createDialog.tooLong';
- } else if (!validateTimelineName(name)) {
- return 'home.createDialog.badFormat';
- } else {
- return null;
- }
- },
- },
- ]}
- onProcess={([name]) => {
- nameSaved = name as string;
- return axios.post(`${apiBaseUrl}/timelines?token=${user.token}`, {
- name,
- });
- }}
- onSuccessAndClose={() => {
- history.push(`timelines/${nameSaved}`);
- }}
- failurePrompt={(e) => `${e as string}`}
- />
- );
-};
-
-export default TimelineCreateDialog;
diff --git a/Timeline/ClientApp/src/app/home/home.sass b/Timeline/ClientApp/src/app/home/home.sass
deleted file mode 100644
index f5d6ffc3..00000000
--- a/Timeline/ClientApp/src/app/home/home.sass
+++ /dev/null
@@ -1,13 +0,0 @@
-.timeline-board-item
- font-size: 1.1em
- @extend .my-2
- .icon
- height: 1.3em
- @extend .mr-2
-
-.timeline-board
- @extend .cru-card
- @extend .d-flex
- @extend .flex-column
- @extend .p-3
- min-height: 200px
diff --git a/Timeline/ClientApp/src/app/i18n.ts b/Timeline/ClientApp/src/app/i18n.ts
deleted file mode 100644
index bda33fe8..00000000
--- a/Timeline/ClientApp/src/app/i18n.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-import i18n, { BackendModule, ResourceKey } from 'i18next';
-import LanguageDetector from 'i18next-browser-languagedetector';
-import { initReactI18next } from 'react-i18next';
-
-const backend: BackendModule = {
- type: 'backend',
- async read(language, namespace, callback) {
- function error(message: string): void {
- callback(new Error(message), false);
- }
-
- function success(result: ResourceKey): void {
- callback(null, result);
- }
-
- if (namespace !== 'translation') {
- error("Namespace must be 'translation'.");
- }
-
- if (language === 'en') {
- const res = (
- await import(
- /* webpackChunkName: "locales-en" */ './locales/en/translation'
- )
- ).default;
- success(res);
- } else if (language === 'zh-cn' || language === 'zh') {
- const res = (
- await import(
- /* webpackChunkName: "locales-zh" */ './locales/zh/translation'
- )
- ).default;
- success(res);
- } else {
- error(`Language ${language} is not supported.`);
- }
- },
- init() {}, // eslint-disable-line @typescript-eslint/no-empty-function
- create() {}, // eslint-disable-line @typescript-eslint/no-empty-function
-};
-
-export const i18nPromise = i18n
- .use(LanguageDetector)
- .use(backend)
- .use(initReactI18next) // bind react-i18next to the instance
- .init({
- fallbackLng: false,
- lowerCaseLng: true,
-
- debug: process.env.NODE_ENV === 'development',
-
- interpolation: {
- escapeValue: false, // not needed for react!!
- },
-
- // react i18next special options (optional)
- // override if needed - omit if ok with defaults
- /*
- react: {
- bindI18n: 'languageChanged',
- bindI18nStore: '',
- transEmptyNodeValue: '',
- transSupportBasicHtmlNodes: true,
- transKeepBasicHtmlNodesFor: ['br', 'strong', 'i'],
- useSuspense: true,
- }
- */
- });
-
-if (module.hot) {
- module.hot.accept(
- ['./locales/en/translation', './locales/zh/translation'],
- () => {
- void i18n.reloadResources();
- }
- );
-}
-
-export default i18n;
diff --git a/Timeline/ClientApp/src/app/index.sass b/Timeline/ClientApp/src/app/index.sass
deleted file mode 100644
index ef0b03ba..00000000
--- a/Timeline/ClientApp/src/app/index.sass
+++ /dev/null
@@ -1,82 +0,0 @@
-@import '~bootstrap/scss/bootstrap'
-
-@import './common/common'
-@import './common/alert'
-@import './home/home'
-@import './about/about'
-@import './timeline/timeline'
-@import './timeline/timeline-ui'
-@import './user/user-page'
-
-body
- margin: 0
-
-#app
- display: flex
- flex-direction: column
-
-small
- line-height: 1.2
-
-.width-1px
- width: 1px
-
-.flex-fix-length
- flex-grow: 0
- flex-shrink: 0
-
-.position-lt
- left: 0
- top: 0
-
-.position-rb
- right: 0
- bottom: 0
-
-.app-bar
- z-index: 1035
-
-.avatar
- width: 60px
-
-.avatar.large
- width: 100px
-
-.avatar.small
- width: 40px
-
-.mt-appbar
- margin-top: 56px
-
-.icon-button
- font-size: 1.4em
-
-.large-icon-button
- font-size: 1.6em
-
-.cursor-pointer
- cursor: pointer
-
-textarea
- resize: none
-
-.white-space-no-wrap
- white-space: nowrap
-
-.cru-card
- @extend .shadow
- @extend .border
- @extend .border-primary
- @extend .rounded
- @extend .bg-light
-
-.full-viewport-center-child
- position: fixed
- width: 100vw
- height: 100vh
- display: flex
- justify-content: center
- align-items: center
-
-.text-orange
- color: $orange
diff --git a/Timeline/ClientApp/src/app/index.tsx b/Timeline/ClientApp/src/app/index.tsx
deleted file mode 100644
index 086bd6e8..00000000
--- a/Timeline/ClientApp/src/app/index.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import 'regenerator-runtime';
-import 'core-js/modules/es.promise';
-import 'core-js/modules/es.array.iterator';
-import 'pepjs';
-
-import React from 'react';
-import ReactDOM from 'react-dom';
-
-import './index.sass';
-
-import './i18n';
-
-import App from './App';
-
-ReactDOM.render(<App />, document.getElementById('app'));
diff --git a/Timeline/ClientApp/src/app/locales/en/translation.ts b/Timeline/ClientApp/src/app/locales/en/translation.ts
deleted file mode 100644
index 6bac41f9..00000000
--- a/Timeline/ClientApp/src/app/locales/en/translation.ts
+++ /dev/null
@@ -1,194 +0,0 @@
-import TranslationResource from '../scheme';
-
-const translation: TranslationResource = {
- welcome: 'Welcome!',
- search: 'Search',
- serviceWorker: {
- availableOffline:
- 'Timeline is now cached in your computer and you can use it offline. 🎉🎉🎉',
- upgradePrompt: 'App is getting a new version!',
- upgradeNow: 'Update Now',
- upgradeSuccess:
- 'Congratulations! App update succeeded! Still you can use it offline. 🎉🎉🎉',
- externalActivatedPrompt:
- 'A new version of app is activated. Please refresh the page. Or it may be broken.',
- reloadNow: 'Refresh Now',
- },
- nav: {
- settings: 'Settings',
- login: 'Login',
- about: 'About',
- },
- chooseImage: 'Choose a image',
- loadImageError: 'Failed to load image.',
- home: {
- go: 'Go!',
- allTimeline: 'All Timelines',
- joinTimeline: 'Joined Timelines',
- ownTimeline: 'Owned Timelines',
- createButton: 'Create Timeline',
- createDialog: {
- title: 'Create Timeline!',
- name: 'Name',
- nameFormat:
- 'Name must consist of only letter including non-English letter, digit, hyphen(-) and underline(_) and be no longer than 26.',
- badFormat: 'Bad format.',
- noEmpty: 'Empty is not allowed.',
- tooLong: 'Too long.',
- },
- },
- operationDialog: {
- retry: 'Retry',
- nextStep: 'Next',
- previousStep: 'Previous',
- confirm: 'Confirm',
- cancel: 'Cancel',
- ok: 'OK!',
- processing: 'Processing...',
- success: 'Success!',
- error: 'An error occured.',
- },
- timeline: {
- messageCantSee: 'Sorry, you are not allowed to see this timeline.😅',
- userNotExist: 'The user does not exist!',
- timelineNotExist: 'The timeline does not exist!',
- manage: 'Manage',
- memberButton: 'Member',
- send: 'Send',
- deletePostFailed: 'Failed to delete post.',
- sendPostFailed: 'Failed to send post.',
- visibility: {
- public: 'public to everyone',
- register: 'only registed people can see',
- private: 'only members can see',
- },
- visibilityTooltip: {
- public:
- 'Everyone including those without accounts can see content of the timeline.',
- register:
- 'Only those who have an account and logined can see content of the timeline.',
- private: 'Only members of this timeline can see content of the timeline.',
- },
- dialogChangeProperty: {
- title: 'Change Timeline Properties',
- visibility: 'Visibility',
- description: 'Description',
- },
- member: {
- alreadyMember: 'The user is already a member.',
- add: 'Add',
- remove: 'Remove',
- },
- manageItem: {
- nickname: 'Nickname',
- avatar: 'Avatar',
- property: 'Timeline Property',
- member: 'Timeline Member',
- delete: 'Delete Timeline',
- },
- deleteDialog: {
- title: 'Delete Timeline',
- inputPrompt:
- 'This is a dangerous action. If you are sure to delete timeline<1>{{name}}</1>, please input its name below and click confirm button.',
- notMatch: 'Name does not match.',
- },
- post: {
- deleteDialog: {
- title: 'Confirm Delete',
- prompt:
- 'Are you sure to delete the post? This operation is not recoverable.',
- },
- },
- },
- user: {
- username: 'username',
- password: 'password',
- login: 'login',
- rememberMe: 'Remember Me',
- welcomeBack: 'Welcome back!',
- verifyTokenFailed: 'User login info is expired. Please login again!',
- verifyTokenFailedNetwork:
- 'Verifying user login info failed. Please check your network and refresh page!',
- },
- login: {
- emptyUsername: "Username can't be empty.",
- emptyPassword: "Password can't be empty.",
- badCredential: 'Username or password is invalid.',
- alreadyLogin: 'Already login! Redirect to home page in 3s!',
- },
- userPage: {
- dialogChangeNickname: {
- title: 'Change Nickname',
- inputLabel: 'New nickname',
- },
- dialogChangeAvatar: {
- title: 'Change Avatar',
- previewImgAlt: 'preview',
- prompt: {
- select: 'Please select a picture.',
- crop: 'Please crop the picture.',
- processingCrop: 'Cropping picture...',
- uploading: 'Uploading...',
- preview: 'Please preview avatar',
- },
- upload: 'upload',
- },
- },
- settings: {
- subheaders: {
- account: 'Account',
- customization: 'Customization',
- },
- languagePrimary: 'Choose display language.',
- languageSecondary:
- 'You language preference will be saved locally. Next time you visit this page, last language option will be used.',
- changePassword: "Change account's password.",
- logout: 'Log out this account.',
- gotoSelf:
- 'Click here to go to timeline of myself to change nickname and avatar.',
- dialogChangePassword: {
- title: 'Change Password',
- prompt:
- 'You are changing your password. You need to input the correct old password. After change, you need to login again and all old login will be invalid.',
- inputOldPassword: 'Old password',
- inputNewPassword: 'New password',
- inputRetypeNewPassword: 'Retype new password',
- errorEmptyOldPassword: "Old password can't be empty.",
- errorEmptyNewPassword: "New password can't be empty.",
- errorRetypeNotMatch: 'Password retyped does not match.',
- },
- dialogConfirmLogout: {
- title: 'Confirm Logout',
- prompt:
- 'Are you sure to log out? All cached data in the browser will be deleted.',
- },
- },
- about: {
- author: {
- title: 'Site Developer',
- fullname: 'Fullname: ',
- nickname: 'Nickname: ',
- introduction: 'Introduction: ',
- introductionContent: 'A programmer coding based on coincidence',
- links: 'Links: ',
- },
- site: {
- title: 'Site Information',
- content:
- 'The name of this site is <1>Timeline</1>, which is a Web App with <3>timeline</3> as its core concept. Its frontend and backend are both developed by <5>me</5>, and open source on GitHub. It is relatively easy to deploy it on your own server, which is also one of my goals. Welcome to comment anything in GitHub repository.',
- repo: 'GitHub Repo',
- },
- credits: {
- title: 'Credits',
- content:
- 'Timeline is works standing on shoulders of gaints. Special appreciation for many open source projects listed below or not. Related licenses could be found in GitHub repository.',
- frontend: 'Frontend: ',
- backend: 'Backend: ',
- },
- },
- admin: {
- title: 'admin',
- },
-};
-
-export default translation;
diff --git a/Timeline/ClientApp/src/app/locales/scheme.ts b/Timeline/ClientApp/src/app/locales/scheme.ts
deleted file mode 100644
index 13d555d5..00000000
--- a/Timeline/ClientApp/src/app/locales/scheme.ts
+++ /dev/null
@@ -1,175 +0,0 @@
-export default interface TranslationResource {
- welcome: string;
- search: string;
- chooseImage: string;
- loadImageError: string;
- serviceWorker: {
- availableOffline: string;
- upgradePrompt: string;
- upgradeNow: string;
- upgradeSuccess: string;
- externalActivatedPrompt: string;
- reloadNow: string;
- };
- nav: {
- settings: string;
- login: string;
- about: string;
- };
- home: {
- go: string;
- allTimeline: string;
- joinTimeline: string;
- ownTimeline: string;
- createButton: string;
- createDialog: {
- title: string;
- name: string;
- nameFormat: string;
- badFormat: string;
- noEmpty: string;
- tooLong: string;
- };
- };
- operationDialog: {
- retry: string;
- nextStep: string;
- previousStep: string;
- confirm: string;
- cancel: string;
- ok: string;
- processing: string;
- success: string;
- error: string;
- };
- timeline: {
- messageCantSee: string;
- userNotExist: string;
- timelineNotExist: string;
- manage: string;
- memberButton: string;
- send: string;
- deletePostFailed: string;
- sendPostFailed: string;
- visibility: {
- public: string;
- register: string;
- private: string;
- };
- visibilityTooltip: {
- public: string;
- register: string;
- private: string;
- };
- dialogChangeProperty: {
- title: string;
- visibility: string;
- description: string;
- };
- member: {
- alreadyMember: string;
- add: string;
- remove: string;
- };
- manageItem: {
- nickname: string;
- avatar: string;
- property: string;
- member: string;
- delete: string;
- };
- deleteDialog: {
- title: string;
- inputPrompt: string;
- notMatch: string;
- };
- post: {
- deleteDialog: {
- title: string;
- prompt: string;
- };
- };
- };
- user: {
- username: string;
- password: string;
- login: string;
- rememberMe: string;
- welcomeBack: string;
- verifyTokenFailed: string;
- verifyTokenFailedNetwork: string;
- };
- login: {
- emptyUsername: string;
- emptyPassword: string;
- badCredential: string;
- alreadyLogin: string;
- };
- userPage: {
- dialogChangeNickname: {
- title: string;
- inputLabel: string;
- };
- dialogChangeAvatar: {
- title: string;
- previewImgAlt: string;
- prompt: {
- select: string;
- crop: string;
- processingCrop: string;
- preview: string;
- uploading: string;
- };
- upload: string;
- };
- };
- settings: {
- subheaders: {
- account: string;
- customization: string;
- };
- languagePrimary: string;
- languageSecondary: string;
- changePassword: string;
- logout: string;
- gotoSelf: string;
- dialogChangePassword: {
- title: string;
- prompt: string;
- inputOldPassword: string;
- inputNewPassword: string;
- inputRetypeNewPassword: string;
- errorEmptyOldPassword: string;
- errorEmptyNewPassword: string;
- errorRetypeNotMatch: string;
- };
- dialogConfirmLogout: {
- title: string;
- prompt: string;
- };
- };
- about: {
- author: {
- title: string;
- fullname: string;
- nickname: string;
- introduction: string;
- introductionContent: string;
- links: string;
- };
- site: {
- title: string;
- content: string;
- repo: string;
- };
- credits: {
- title: string;
- content: string;
- frontend: string;
- backend: string;
- };
- };
- admin: {
- title: string;
- };
-}
diff --git a/Timeline/ClientApp/src/app/locales/zh/translation.ts b/Timeline/ClientApp/src/app/locales/zh/translation.ts
deleted file mode 100644
index fba06c7f..00000000
--- a/Timeline/ClientApp/src/app/locales/zh/translation.ts
+++ /dev/null
@@ -1,187 +0,0 @@
-import TranslationResource from '../scheme';
-
-const translation: TranslationResource = {
- welcome: '欢迎!',
- search: '搜索',
- serviceWorker: {
- availableOffline: 'Timeline 已经缓存在本地,你可以离线使用它。🎉🎉🎉',
- upgradePrompt: 'App 有新版本!',
- upgradeNow: '现在升级',
- upgradeSuccess: 'App 升级成功,当然,你仍可以离线使用它。 🎉🎉🎉',
- externalActivatedPrompt:
- '一个新的 App 版本已经激活,请刷新页面使用,否则页面可能会出现故障。',
- reloadNow: '立刻刷新',
- },
- nav: {
- settings: '设置',
- login: '登陆',
- about: '关于',
- },
- chooseImage: '选择一个图片',
- loadImageError: '加载图片失败',
- home: {
- go: '冲!',
- allTimeline: '所有的时间线',
- joinTimeline: '加入的时间线',
- ownTimeline: '拥有的时间线',
- createButton: '创建时间线',
- createDialog: {
- title: '创建时间线!',
- name: '名字',
- nameFormat:
- '名字只能由字母、汉字、数字、下划线(_)和连字符(-)构成,且长度不能超过26.',
- badFormat: '格式错误',
- noEmpty: '不能为空',
- tooLong: '太长了',
- },
- },
- operationDialog: {
- retry: '重试',
- nextStep: '下一步',
- previousStep: '上一步',
- confirm: '确定',
- cancel: '取消',
- ok: '好的!',
- processing: '处理中...',
- success: '成功!',
- error: '出错啦!',
- },
- timeline: {
- messageCantSee: '不好意思,你没有权限查看这个时间线。😅',
- userNotExist: '该用户不存在!',
- timelineNotExist: '该时间线不存在!',
- manage: '管理',
- memberButton: '成员',
- send: '发送',
- deletePostFailed: '删除消息失败。',
- sendPostFailed: '发送消息失败。',
- visibility: {
- public: '对所有人公开',
- register: '仅注册可见',
- private: '仅成员可见',
- },
- visibilityTooltip: {
- public: '所有人都可以看到这个时间线的内容,包括没有注册的人。',
- register: '只有拥有本网站的账号且登陆了的人才能看到这个时间线的内容。',
- private: '只有这个时间线的成员可以看到这个时间线的内容。',
- },
- dialogChangeProperty: {
- title: '修改时间线属性',
- visibility: '可见性',
- description: '描述',
- },
- member: {
- alreadyMember: '该用户已经是一个成员。',
- add: '添加',
- remove: '移除',
- },
- manageItem: {
- nickname: '昵称',
- avatar: '头像',
- property: '时间线属性',
- member: '时间线成员',
- delete: '删除时间线',
- },
- deleteDialog: {
- title: '删除时间线',
- inputPrompt:
- '这是一个危险的操作。如果您确认要删除时间线<1>{{name}}</1>,请在下面输入它的名字并点击确认。',
- notMatch: '名字不匹配',
- },
- post: {
- deleteDialog: {
- title: '确认删除',
- prompt: '确定删除这个消息?这个操作不可撤销。',
- },
- },
- },
- user: {
- username: '用户名',
- password: '密码',
- login: '登录',
- rememberMe: '记住我',
- welcomeBack: '欢迎回来!',
- verifyTokenFailed: '用户登录信息已过期,请重新登陆!',
- verifyTokenFailedNetwork:
- '验证用户登录信息失败,请检查网络连接并刷新页面!',
- },
- login: {
- emptyUsername: '用户名不能为空。',
- emptyPassword: '密码不能为空。',
- badCredential: '用户名或密码错误。',
- alreadyLogin: '已经登陆,三秒后导航到首页!',
- },
- userPage: {
- dialogChangeNickname: {
- title: '更改昵称',
- inputLabel: '新昵称',
- },
- dialogChangeAvatar: {
- title: '修改头像',
- previewImgAlt: '预览',
- prompt: {
- select: '请选择一个图片',
- crop: '请裁剪图片',
- processingCrop: '正在裁剪图片',
- uploading: '正在上传',
- preview: '请预览图片',
- },
- upload: '上传',
- },
- },
- settings: {
- subheaders: {
- account: '账户',
- customization: '个性化',
- },
- languagePrimary: '选择显示的语言。',
- languageSecondary:
- '您的语言偏好将会存储在本地,下次浏览时将自动使用上次保存的语言选项。',
- changePassword: '更改账号的密码。',
- logout: '注销此账号。',
- gotoSelf: '点击前往个人时间线修改昵称和头像!',
- dialogChangePassword: {
- title: '修改密码',
- prompt:
- '您正在修改密码,您需要输入正确的旧密码。成功修改后您需要重新登陆,而且以前所有的登录都会失效。',
- inputOldPassword: '旧密码',
- inputNewPassword: '新密码',
- inputRetypeNewPassword: '再次输入新密码',
- errorEmptyOldPassword: '旧密码不能为空。',
- errorEmptyNewPassword: '新密码不能为空',
- errorRetypeNotMatch: '两次输入的密码不一致',
- },
- dialogConfirmLogout: {
- title: '确定注销',
- prompt: '您确定注销此账号?这将删除所有已经缓存在浏览器的数据。',
- },
- },
- about: {
- author: {
- title: '网站作者',
- fullname: '姓名:',
- nickname: '昵称:',
- introduction: '简介:',
- introductionContent: '一个基于巧合编程的代码爱好者。',
- links: '链接:',
- },
- site: {
- title: '网站信息',
- content:
- '这个网站的名字叫 <1>Timeline</1>,是一个以<3>时间线</3>为核心概念的 Web App . 它的前端和后端都是由<5>我</5>开发,并且在 GitHub 上开源。大家可以相对轻松的把它们部署在自己的服务器上,这也是我的目标之一。欢迎大家前往 GitHub 仓库提出任何意见。',
- repo: 'GitHub 仓库',
- },
- credits: {
- title: '鸣谢',
- content:
- 'Timeline 是站在巨人肩膀上的作品,感谢以下列出的和其他未列出的许多开源项目,相关 License 请在 GitHub 仓库中查看。',
- frontend: '前端:',
- backend: '后端:',
- },
- },
- admin: {
- title: '管理',
- },
-};
-
-export default translation;
diff --git a/Timeline/ClientApp/src/app/service-worker.tsx b/Timeline/ClientApp/src/app/service-worker.tsx
deleted file mode 100644
index 0dbab480..00000000
--- a/Timeline/ClientApp/src/app/service-worker.tsx
+++ /dev/null
@@ -1,109 +0,0 @@
-import React from 'react';
-import { Button } from 'reactstrap';
-import { useTranslation } from 'react-i18next';
-
-import { pushAlert } from './common/alert-service';
-
-if ('serviceWorker' in navigator) {
- let isThisTriggerUpgrade = false;
-
- const upgradeSuccessLocalStorageKey = 'TIMELINE_UPGRADE_SUCCESS';
-
- if (window.localStorage.getItem(upgradeSuccessLocalStorageKey)) {
- pushAlert({
- message: {
- type: 'i18n',
- key: 'serviceWorker.upgradeSuccess',
- },
- type: 'success',
- });
- window.localStorage.removeItem(upgradeSuccessLocalStorageKey);
- }
-
- void import('workbox-window').then(({ Workbox, messageSW }) => {
- const wb = new Workbox('/sw.js');
- let registration: ServiceWorkerRegistration | undefined;
-
- // externalactivated is not usable but I still use its name.
- wb.addEventListener('controlling', () => {
- const upgradeReload = (): void => {
- window.localStorage.setItem(upgradeSuccessLocalStorageKey, 'true');
- window.location.reload();
- };
-
- if (isThisTriggerUpgrade) {
- upgradeReload();
- } else {
- const Message: React.FC = () => {
- const { t } = useTranslation();
- return (
- <>
- {t('serviceWorker.externalActivatedPrompt')}
- <Button color="success" size="sm" onClick={upgradeReload} outline>
- {t('serviceWorker.reloadNow')}
- </Button>
- </>
- );
- };
-
- pushAlert({
- message: Message,
- dismissTime: 'never',
- type: 'warning',
- });
- }
- });
-
- wb.addEventListener('activated', (event) => {
- if (!event.isUpdate) {
- pushAlert({
- message: {
- type: 'i18n',
- key: 'serviceWorker.availableOffline',
- },
- type: 'success',
- });
- }
- });
-
- const showSkipWaitingPrompt = (): void => {
- const upgrade = (): void => {
- isThisTriggerUpgrade = true;
- if (registration && registration.waiting) {
- // Send a message to the waiting service worker,
- // instructing it to activate.
- // Note: for this to work, you have to add a message
- // listener in your service worker. See below.
- void messageSW(registration.waiting, { type: 'SKIP_WAITING' });
- }
- };
-
- const UpgradeMessage: React.FC = () => {
- const { t } = useTranslation();
- return (
- <>
- {t('serviceWorker.upgradePrompt')}
- <Button color="success" size="sm" onClick={upgrade} outline>
- {t('serviceWorker.upgradeNow')}
- </Button>
- </>
- );
- };
-
- pushAlert({
- message: UpgradeMessage,
- dismissTime: 'never',
- type: 'success',
- });
- };
-
- // Add an event listener to detect when the registered
- // service worker has installed but is waiting to activate.
- wb.addEventListener('waiting', showSkipWaitingPrompt);
- wb.addEventListener('externalwaiting', showSkipWaitingPrompt);
-
- void wb.register().then((reg) => {
- registration = reg;
- });
- });
-}
diff --git a/Timeline/ClientApp/src/app/settings/Settings.tsx b/Timeline/ClientApp/src/app/settings/Settings.tsx
deleted file mode 100644
index 96a3fab4..00000000
--- a/Timeline/ClientApp/src/app/settings/Settings.tsx
+++ /dev/null
@@ -1,253 +0,0 @@
-import React, { useState } from 'react';
-import { useHistory } from 'react-router';
-import { useTranslation } from 'react-i18next';
-import axios, { AxiosError } from 'axios';
-import {
- Container,
- Row,
- Col,
- Input,
- Modal,
- ModalHeader,
- ModalBody,
- ModalFooter,
- Button,
-} from 'reactstrap';
-
-import { apiBaseUrl } from '../config';
-
-import { useUser, userLogout, useUserLoggedIn } from '../data/user';
-
-import AppBar from '../common/AppBar';
-import OperationDialog, {
- OperationInputErrorInfo,
-} from '../common/OperationDialog';
-import { CommonErrorResponse } from '../data/common';
-
-interface ChangePasswordDialogProps {
- open: boolean;
- close: () => void;
-}
-
-async function changePassword(
- oldPassword: string,
- newPassword: string,
- token: string
-): Promise<void> {
- const url = `${apiBaseUrl}/userop/changepassword?token=${token}`;
- try {
- await axios.post(url, {
- oldPassword,
- newPassword,
- });
- } catch (e) {
- const error = e as AxiosError<CommonErrorResponse>;
- if (
- error.response &&
- error.response.status === 400 &&
- error.response.data &&
- error.response.data.message
- ) {
- throw error.response.data.message;
- }
- throw e;
- }
-}
-
-const ChangePasswordDialog: React.FC<ChangePasswordDialogProps> = (props) => {
- const user = useUserLoggedIn();
- const history = useHistory();
- const { t } = useTranslation();
-
- const [redirect, setRedirect] = useState<boolean>(false);
-
- return (
- <OperationDialog
- open={props.open}
- title={t('settings.dialogChangePassword.title')}
- titleColor="dangerous"
- inputPrompt={t('settings.dialogChangePassword.prompt')}
- inputScheme={[
- {
- type: 'text',
- label: t('settings.dialogChangePassword.inputOldPassword'),
- password: true,
- validator: (v) =>
- v === ''
- ? 'settings.dialogChangePassword.errorEmptyOldPassword'
- : null,
- },
- {
- type: 'text',
- label: t('settings.dialogChangePassword.inputNewPassword'),
- password: true,
- validator: (v, values) => {
- const error: OperationInputErrorInfo = {};
- error[1] =
- v === ''
- ? 'settings.dialogChangePassword.errorEmptyNewPassword'
- : null;
- if (v === values[2]) {
- error[2] = null;
- } else {
- if (values[2] !== '') {
- error[2] = 'settings.dialogChangePassword.errorRetypeNotMatch';
- }
- }
- return error;
- },
- },
- {
- type: 'text',
- label: t('settings.dialogChangePassword.inputRetypeNewPassword'),
- password: true,
- validator: (v, values) =>
- v !== values[1]
- ? 'settings.dialogChangePassword.errorRetypeNotMatch'
- : null,
- },
- ]}
- onProcess={async ([oldPassword, newPassword]) => {
- await changePassword(
- oldPassword as string,
- newPassword as string,
- user.token
- );
- userLogout();
- setRedirect(true);
- }}
- close={() => {
- props.close();
- if (redirect) {
- history.push('/login');
- }
- }}
- />
- );
-};
-
-const ConfirmLogoutDialog: React.FC<{
- toggle: () => void;
- onConfirm: () => void;
-}> = ({ toggle, onConfirm }) => {
- const { t } = useTranslation();
-
- return (
- <Modal isOpen centered>
- <ModalHeader className="text-danger">
- {t('settings.dialogConfirmLogout.title')}
- </ModalHeader>
- <ModalBody>{t('settings.dialogConfirmLogout.prompt')}</ModalBody>
- <ModalFooter>
- <Button color="secondary" onClick={toggle}>
- {t('operationDialog.cancel')}
- </Button>
- <Button color="danger" onClick={onConfirm}>
- {t('operationDialog.confirm')}
- </Button>
- </ModalFooter>
- </Modal>
- );
-};
-
-const Settings: React.FC = (_) => {
- const { i18n, t } = useTranslation();
- const user = useUser();
- const history = useHistory();
-
- const [dialog, setDialog] = useState<null | 'changepassword' | 'logout'>(
- null
- );
-
- const language = i18n.language.slice(0, 2);
-
- return (
- <>
- <AppBar />
- <Container fluid className="mt-appbar">
- {user ? (
- <>
- <Row className="border-bottom p-3 cursor-pointer">
- <Col xs="12">
- <h5
- onClick={() => {
- history.push(`/users/${user.username}`);
- }}
- >
- {t('settings.gotoSelf')}
- </h5>
- </Col>
- </Row>
- <Row className="border-bottom p-3 cursor-pointer">
- <Col xs="12">
- <h5
- className="text-danger"
- onClick={() => setDialog('changepassword')}
- >
- {t('settings.changePassword')}
- </h5>
- </Col>
- </Row>
- <Row className="border-bottom p-3 cursor-pointer">
- <Col xs="12">
- <h5
- className="text-danger"
- onClick={() => {
- setDialog('logout');
- }}
- >
- {t('settings.logout')}
- </h5>
- </Col>
- </Row>
- </>
- ) : null}
- <Row className="align-items-center border-bottom p-3">
- <Col xs="12" sm="auto">
- <h5>{t('settings.languagePrimary')}</h5>
- <p>{t('settings.languageSecondary')}</p>
- </Col>
- <Col xs="auto" className="ml-auto">
- <Input
- type="select"
- value={language}
- onChange={(e) => {
- void i18n.changeLanguage(e.target.value);
- }}
- >
- <option value="zh">中文</option>
- <option value="en">English</option>
- </Input>
- </Col>
- </Row>
- {(() => {
- switch (dialog) {
- case 'changepassword':
- return (
- <ChangePasswordDialog
- open
- close={() => {
- setDialog(null);
- }}
- />
- );
- case 'logout':
- return (
- <ConfirmLogoutDialog
- toggle={() => setDialog(null)}
- onConfirm={() => {
- userLogout();
- history.push('/');
- }}
- />
- );
- default:
- return null;
- }
- })()}
- </Container>
- </>
- );
-};
-
-export default Settings;
diff --git a/Timeline/ClientApp/src/app/timeline/Timeline.tsx b/Timeline/ClientApp/src/app/timeline/Timeline.tsx
deleted file mode 100644
index 0a68c5db..00000000
--- a/Timeline/ClientApp/src/app/timeline/Timeline.tsx
+++ /dev/null
@@ -1,102 +0,0 @@
-import React from 'react';
-import clsx from 'clsx';
-
-import { TimelinePostInfo } from '../data/timeline';
-import { useUser } from '../data/user';
-import { useAvatarVersion } from '../user/api';
-
-import TimelineItem from './TimelineItem';
-
-export interface TimelinePostInfoEx extends TimelinePostInfo {
- deletable: boolean;
-}
-
-export type TimelineDeleteCallback = (index: number, id: number) => void;
-
-export interface TimelineProps {
- className?: string;
- posts: TimelinePostInfoEx[];
- onDelete: TimelineDeleteCallback;
- onResize?: () => void;
- containerRef?: React.Ref<HTMLDivElement>;
-}
-
-const Timeline: React.FC<TimelineProps> = (props) => {
- const user = useUser();
- const avatarVersion = useAvatarVersion();
-
- const { posts, onDelete, onResize } = props;
-
- const [indexShowDeleteButton, setIndexShowDeleteButton] = React.useState<
- number
- >(-1);
-
- const onItemClick = React.useCallback(() => {
- setIndexShowDeleteButton(-1);
- }, []);
-
- const onToggleDelete = React.useMemo(() => {
- return posts.map((post, i) => {
- return post.deletable
- ? () => {
- setIndexShowDeleteButton((oldIndexShowDeleteButton) => {
- return oldIndexShowDeleteButton !== i ? i : -1;
- });
- }
- : undefined;
- });
- }, [posts]);
-
- const onItemDelete = React.useMemo(() => {
- return posts.map((post, i) => {
- return () => {
- onDelete(i, post.id);
- };
- });
- }, [posts, onDelete]);
-
- return (
- <div
- ref={props.containerRef}
- className={clsx(
- 'container-fluid d-flex flex-column position-relative',
- props.className
- )}
- >
- <div className="timeline-enter-animation-mask" />
- {(() => {
- const length = posts.length;
- return posts.map((post, i) => {
- const av: number | undefined =
- user != null && user.username === post.author.username
- ? avatarVersion
- : undefined;
-
- const toggleMore = onToggleDelete[i];
-
- return (
- <TimelineItem
- post={post}
- key={post.id}
- current={length - 1 === i}
- more={
- toggleMore
- ? {
- isOpen: indexShowDeleteButton === i,
- toggle: toggleMore,
- onDelete: onItemDelete[i],
- }
- : undefined
- }
- onClick={onItemClick}
- avatarVersion={av}
- onResize={onResize}
- />
- );
- });
- })()}
- </div>
- );
-};
-
-export default Timeline;
diff --git a/Timeline/ClientApp/src/app/timeline/TimelineDeleteDialog.tsx b/Timeline/ClientApp/src/app/timeline/TimelineDeleteDialog.tsx
deleted file mode 100644
index 5e7c6dd4..00000000
--- a/Timeline/ClientApp/src/app/timeline/TimelineDeleteDialog.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-import React from 'react';
-import axios from 'axios';
-import { useHistory } from 'react-router';
-import { Trans } from 'react-i18next';
-
-import { apiBaseUrl } from '../config';
-import { useUserLoggedIn } from '../data/user';
-import OperationDialog from '../common/OperationDialog';
-
-interface TimelineDeleteDialog {
- open: boolean;
- name: string;
- close: () => void;
-}
-
-const TimelineDeleteDialog: React.FC<TimelineDeleteDialog> = (props) => {
- const user = useUserLoggedIn();
- const history = useHistory();
-
- const { name } = props;
-
- return (
- <OperationDialog
- open={props.open}
- close={props.close}
- title="timeline.deleteDialog.title"
- titleColor="danger"
- inputPrompt={() => {
- return (
- <Trans i18nKey="timeline.deleteDialog.inputPrompt">
- 0<code className="mx-2">{{ name }}</code>2
- </Trans>
- );
- }}
- inputScheme={[
- {
- type: 'text',
- validator: (value) => {
- if (value !== name) {
- return 'timeline.deleteDialog.notMatch';
- } else {
- return null;
- }
- },
- },
- ]}
- onProcess={() => {
- return axios.delete(
- `${apiBaseUrl}/timelines/${name}?token=${user.token}`
- );
- }}
- onSuccessAndClose={() => {
- history.replace('/');
- }}
- />
- );
-};
-
-export default TimelineDeleteDialog;
diff --git a/Timeline/ClientApp/src/app/timeline/TimelineInfoCard.tsx b/Timeline/ClientApp/src/app/timeline/TimelineInfoCard.tsx
deleted file mode 100644
index e4bc07d1..00000000
--- a/Timeline/ClientApp/src/app/timeline/TimelineInfoCard.tsx
+++ /dev/null
@@ -1,109 +0,0 @@
-import React from 'react';
-import clsx from 'clsx';
-import {
- Dropdown,
- DropdownToggle,
- DropdownMenu,
- DropdownItem,
- Button,
-} from 'reactstrap';
-import { useTranslation } from 'react-i18next';
-import { fromEvent } from 'rxjs';
-
-import {
- timelineVisibilityTooltipTranslationMap,
- TimelineInfo,
-} from '../data/timeline';
-import { TimelineCardComponentProps } from './TimelinePageTemplateUI';
-
-export type OrdinaryTimelineManageItem = 'delete';
-
-export type TimelineInfoCardProps = TimelineCardComponentProps<
- TimelineInfo,
- OrdinaryTimelineManageItem
->;
-
-const TimelineInfoCard: React.FC<TimelineInfoCardProps> = (props) => {
- const { onHeight, onManage } = props;
-
- const { t } = useTranslation();
-
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- const containerRef = React.useRef<HTMLDivElement>(null!);
-
- const notifyHeight = React.useCallback((): void => {
- if (onHeight) {
- onHeight(containerRef.current.getBoundingClientRect().height);
- }
- }, [onHeight]);
-
- React.useEffect(() => {
- const subscription = fromEvent(window, 'resize').subscribe(notifyHeight);
- return () => subscription.unsubscribe();
- });
-
- const [manageDropdownOpen, setManageDropdownOpen] = React.useState<boolean>(
- false
- );
- const toggleManageDropdown = React.useCallback(
- (): void => setManageDropdownOpen((old) => !old),
- []
- );
-
- return (
- <div
- ref={containerRef}
- className={clsx('rounded border p-2 bg-light', props.className)}
- onTransitionEnd={notifyHeight}
- >
- <h3 className="text-primary mx-3 d-inline-block align-middle">
- {props.timeline.name}
- </h3>
- <div className="d-inline-block align-middle">
- <img
- src={props.timeline.owner._links.avatar}
- onLoad={notifyHeight}
- className="avatar small rounded-circle"
- />
- {props.timeline.owner.nickname}
- <small className="ml-3 text-secondary">
- @{props.timeline.owner.username}
- </small>
- </div>
- <p className="mb-0">{props.timeline.description}</p>
- <small className="mt-1 d-block">
- {t(timelineVisibilityTooltipTranslationMap[props.timeline.visibility])}
- </small>
- <div className="text-right mt-2">
- {onManage != null ? (
- <Dropdown isOpen={manageDropdownOpen} toggle={toggleManageDropdown}>
- <DropdownToggle outline color="primary">
- {t('timeline.manage')}
- </DropdownToggle>
- <DropdownMenu>
- <DropdownItem onClick={() => onManage('property')}>
- {t('timeline.manageItem.property')}
- </DropdownItem>
- <DropdownItem onClick={props.onMember}>
- {t('timeline.manageItem.member')}
- </DropdownItem>
- <DropdownItem divider />
- <DropdownItem
- className="text-danger"
- onClick={() => onManage('delete')}
- >
- {t('timeline.manageItem.delete')}
- </DropdownItem>
- </DropdownMenu>
- </Dropdown>
- ) : (
- <Button color="primary" outline onClick={props.onMember}>
- {t('timeline.memberButton')}
- </Button>
- )}
- </div>
- </div>
- );
-};
-
-export default TimelineInfoCard;
diff --git a/Timeline/ClientApp/src/app/timeline/TimelineItem.tsx b/Timeline/ClientApp/src/app/timeline/TimelineItem.tsx
deleted file mode 100644
index 43e206f1..00000000
--- a/Timeline/ClientApp/src/app/timeline/TimelineItem.tsx
+++ /dev/null
@@ -1,181 +0,0 @@
-import React from 'react';
-import clsx from 'clsx';
-import {
- Row,
- Col,
- Modal,
- ModalHeader,
- ModalBody,
- ModalFooter,
- Button,
-} from 'reactstrap';
-import { Link } from 'react-router-dom';
-import { useTranslation } from 'react-i18next';
-import Svg from 'react-inlinesvg';
-
-import chevronDownIcon from 'bootstrap-icons/icons/chevron-down.svg';
-import trashIcon from 'bootstrap-icons/icons/trash.svg';
-
-import { TimelinePostInfo } from '../data/timeline';
-import { useAvatarUrlWithGivenVersion } from '../user/api';
-
-const TimelinePostDeleteConfirmDialog: React.FC<{
- toggle: () => void;
- onConfirm: () => void;
-}> = ({ toggle, onConfirm }) => {
- const { t } = useTranslation();
-
- return (
- <Modal toggle={toggle} isOpen centered>
- <ModalHeader className="text-danger">
- {t('timeline.post.deleteDialog.title')}
- </ModalHeader>
- <ModalBody>{t('timeline.post.deleteDialog.prompt')}</ModalBody>
- <ModalFooter>
- <Button color="secondary" onClick={toggle}>
- {t('operationDialog.cancel')}
- </Button>
- <Button
- color="danger"
- onClick={() => {
- onConfirm();
- toggle();
- }}
- >
- {t('operationDialog.confirm')}
- </Button>
- </ModalFooter>
- </Modal>
- );
-};
-
-export interface TimelineItemProps {
- post: TimelinePostInfo;
- current?: boolean;
- more?: {
- isOpen: boolean;
- toggle: () => void;
- onDelete: () => void;
- };
- onClick?: () => void;
- avatarVersion?: number;
- onResize?: () => void;
- className?: string;
- style?: React.CSSProperties;
-}
-
-const TimelineItem: React.FC<TimelineItemProps> = (props) => {
- const { i18n } = useTranslation();
-
- const current = props.current === true;
-
- const { more, onResize } = props;
-
- const avatarUrl = useAvatarUrlWithGivenVersion(
- props.avatarVersion,
- props.post.author._links.avatar
- );
-
- const [deleteDialog, setDeleteDialog] = React.useState<boolean>(false);
- const toggleDeleteDialog = React.useCallback(
- () => setDeleteDialog((old) => !old),
- []
- );
-
- return (
- <Row
- className={clsx(
- 'position-relative flex-nowrap',
- current && 'current',
- props.className
- )}
- onClick={props.onClick}
- style={props.style}
- >
- <Col className="timeline-line-area">
- <div className="timeline-line-segment start"></div>
- <div className="timeline-line-node-container">
- <div className="timeline-line-node"></div>
- </div>
- <div className="timeline-line-segment end"></div>
- {current && <div className="timeline-line-segment current-end" />}
- </Col>
- <Col className="timeline-pt-start">
- <Row className="flex-nowrap">
- <div className="col-auto flex-shrink-1 px-0">
- <Row className="ml-n3 mr-0 align-items-center">
- <span className="ml-3 text-primary white-space-no-wrap">
- {props.post.time.toLocaleString(i18n.languages)}
- </span>
- <small className="text-dark ml-3">
- {props.post.author.nickname}
- </small>
- </Row>
- </div>
- {more != null ? (
- <div className="col-auto px-2 d-flex justify-content-center align-items-center">
- <Svg
- src={chevronDownIcon}
- className="text-info icon-button"
- onClick={(e: Event) => {
- more.toggle();
- e.stopPropagation();
- }}
- />
- </div>
- ) : null}
- </Row>
- <div className="row d-block timeline-content">
- <Link
- className="float-right float-sm-left mx-2"
- to={'/users/' + props.post.author.username}
- >
- <img onLoad={onResize} src={avatarUrl} className="avatar rounded" />
- </Link>
- {(() => {
- const { content } = props.post;
- if (content.type === 'text') {
- return content.text;
- } else {
- return (
- <img
- onLoad={onResize}
- src={content.url}
- className="timeline-content-image"
- />
- );
- }
- })()}
- </div>
- </Col>
- {more != null && more.isOpen ? (
- <>
- <div
- className="position-absolute position-lt w-100 h-100 mask d-flex justify-content-center align-items-center"
- onClick={more.toggle}
- >
- <Svg
- src={trashIcon}
- className="text-danger large-icon-button"
- onClick={(e: Event) => {
- toggleDeleteDialog();
- e.stopPropagation();
- }}
- />
- </div>
- {deleteDialog ? (
- <TimelinePostDeleteConfirmDialog
- toggle={() => {
- toggleDeleteDialog();
- more.toggle();
- }}
- onConfirm={more.onDelete}
- />
- ) : null}
- </>
- ) : null}
- </Row>
- );
-};
-
-export default TimelineItem;
diff --git a/Timeline/ClientApp/src/app/timeline/TimelineMember.tsx b/Timeline/ClientApp/src/app/timeline/TimelineMember.tsx
deleted file mode 100644
index f9747b4d..00000000
--- a/Timeline/ClientApp/src/app/timeline/TimelineMember.tsx
+++ /dev/null
@@ -1,196 +0,0 @@
-import React, { useState } from 'react';
-import { useTranslation } from 'react-i18next';
-
-import { User } from '../data/user';
-
-import SearchInput from '../common/SearchInput';
-import {
- Container,
- ListGroup,
- ListGroupItem,
- Modal,
- Row,
- Col,
- Button,
-} from 'reactstrap';
-
-export interface TimelineMemberCallbacks {
- onCheckUser: (username: string) => Promise<User | null>;
- onAddUser: (user: User) => Promise<void>;
- onRemoveUser: (username: string) => void;
-}
-
-export interface TimelineMemberProps {
- members: User[];
- edit: TimelineMemberCallbacks | null | undefined;
-}
-
-const TimelineMember: React.FC<TimelineMemberProps> = (props) => {
- const { t } = useTranslation();
-
- const [userSearchText, setUserSearchText] = useState<string>('');
- const [userSearchState, setUserSearchState] = useState<
- | {
- type: 'user';
- data: User;
- }
- | { type: 'error'; data: string }
- | { type: 'loading' }
- | { type: 'init' }
- >({ type: 'init' });
-
- const members = props.members;
-
- return (
- <Container className="px-4">
- <ListGroup className="my-3">
- {members.map((member, index) => (
- <ListGroupItem key={member.username} className="container">
- <Row>
- <Col className="col-auto">
- <img src={member._links.avatar} className="avatar small" />
- </Col>
- <Col>
- <Row>{member.nickname}</Row>
- <Row>
- <small>{'@' + member.username}</small>
- </Row>
- </Col>
- {(() => {
- if (index === 0) {
- return null;
- }
- const onRemove = props.edit?.onRemoveUser;
- if (onRemove == null) {
- return null;
- }
- return (
- <Button
- className="align-self-center"
- color="danger"
- onClick={() => {
- onRemove(member.username);
- }}
- >
- {t('timeline.member.remove')}
- </Button>
- );
- })()}
- </Row>
- </ListGroupItem>
- ))}
- </ListGroup>
- {(() => {
- const edit = props.edit;
- if (edit != null) {
- return (
- <>
- <SearchInput
- value={userSearchText}
- onChange={(v) => {
- setUserSearchText(v);
- }}
- loading={userSearchState.type === 'loading'}
- onButtonClick={() => {
- if (userSearchText === '') {
- setUserSearchState({
- type: 'error',
- data: 'login.emptyUsername',
- });
- return;
- }
-
- setUserSearchState({ type: 'loading' });
- edit.onCheckUser(userSearchText).then(
- (u) => {
- if (u == null) {
- setUserSearchState({
- type: 'error',
- data: 'timeline.userNotExist',
- });
- } else {
- setUserSearchState({ type: 'user', data: u });
- }
- },
- (e) => {
- setUserSearchState({
- type: 'error',
- data: `${e as string}`,
- });
- }
- );
- }}
- />
- {(() => {
- if (userSearchState.type === 'user') {
- const u = userSearchState.data;
- const addable =
- members.findIndex((m) => m.username === u.username) === -1;
- return (
- <>
- {!addable ? (
- <p>{t('timeline.member.alreadyMember')}</p>
- ) : null}
- <Container className="mb-3">
- <Row>
- <Col className="col-auto">
- <img
- src={u._links.avatar}
- className="avatar small"
- />
- </Col>
- <Col>
- <Row>{u.nickname}</Row>
- <Row>
- <small>{'@' + u.username}</small>
- </Row>
- </Col>
- <Button
- color="primary"
- className="align-self-center"
- disabled={!addable}
- onClick={() => {
- void edit.onAddUser(u).then((_) => {
- setUserSearchText('');
- setUserSearchState({ type: 'init' });
- });
- }}
- >
- {t('timeline.member.add')}
- </Button>
- </Row>
- </Container>
- </>
- );
- } else if (userSearchState.type === 'error') {
- return (
- <p className="text-danger">{t(userSearchState.data)}</p>
- );
- }
- })()}
- </>
- );
- } else {
- return null;
- }
- })()}
- </Container>
- );
-};
-
-export default TimelineMember;
-
-export interface TimelineMemberDialogProps extends TimelineMemberProps {
- open: boolean;
- onClose: () => void;
-}
-
-export const TimelineMemberDialog: React.FC<TimelineMemberDialogProps> = (
- props
-) => {
- return (
- <Modal isOpen={props.open} toggle={props.onClose}>
- <TimelineMember {...props} />
- </Modal>
- );
-};
diff --git a/Timeline/ClientApp/src/app/timeline/TimelinePage.tsx b/Timeline/ClientApp/src/app/timeline/TimelinePage.tsx
deleted file mode 100644
index 900d6e6a..00000000
--- a/Timeline/ClientApp/src/app/timeline/TimelinePage.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import React from 'react';
-import { useParams } from 'react-router';
-
-import { ordinaryTimelineService } from '../data/timeline';
-
-import TimelinePageUI from './TimelinePageUI';
-import TimelinePageTemplate from '../timeline/TimelinePageTemplate';
-import { OrdinaryTimelineManageItem } from './TimelineInfoCard';
-import TimelineDeleteDialog from './TimelineDeleteDialog';
-
-const TimelinePage: React.FC = _ => {
- const { name } = useParams<{ name: string }>();
-
- const [dialog, setDialog] = React.useState<OrdinaryTimelineManageItem | null>(
- null
- );
-
- let dialogElement: React.ReactElement | undefined;
- if (dialog === 'delete') {
- dialogElement = (
- <TimelineDeleteDialog open close={() => setDialog(null)} name={name} />
- );
- }
-
- return (
- <>
- <TimelinePageTemplate
- name={name}
- UiComponent={TimelinePageUI}
- onManage={item => setDialog(item)}
- service={ordinaryTimelineService}
- notFoundI18nKey="timeline.timelineNotExist"
- />
- {dialogElement}
- </>
- );
-};
-
-export default TimelinePage;
diff --git a/Timeline/ClientApp/src/app/timeline/TimelinePageTemplate.tsx b/Timeline/ClientApp/src/app/timeline/TimelinePageTemplate.tsx
deleted file mode 100644
index 38ecd8f9..00000000
--- a/Timeline/ClientApp/src/app/timeline/TimelinePageTemplate.tsx
+++ /dev/null
@@ -1,275 +0,0 @@
-import React from 'react';
-import { useTranslation } from 'react-i18next';
-import { AxiosError } from 'axios';
-import concat from 'lodash/concat';
-import without from 'lodash/without';
-
-import { ExcludeKey } from '../type-utilities';
-import { useUser, fetchUser } from '../data/user';
-import { pushAlert } from '../common/alert-service';
-import { extractStatusCode, extractErrorCode } from '../data/common';
-import {
- TimelineServiceTemplate,
- TimelineInfo,
- TimelineChangePropertyRequest,
-} from '../data/timeline';
-
-import { TimelinePostInfoEx, TimelineDeleteCallback } from './Timeline';
-import { TimelineMemberDialog } from './TimelineMember';
-import TimelinePropertyChangeDialog from './TimelinePropertyChangeDialog';
-import { TimelinePageTemplateUIProps } from './TimelinePageTemplateUI';
-import { TimelinePostSendCallback } from './TimelinePostEdit';
-import { UiLogicError } from '../common';
-
-export interface TimelinePageTemplateProps<
- TManageItem,
- TTimeline extends TimelineInfo
-> {
- name: string;
- onManage: (item: TManageItem) => void;
- service: TimelineServiceTemplate<TTimeline, TimelineChangePropertyRequest>;
- UiComponent: React.ComponentType<
- ExcludeKey<
- TimelinePageTemplateUIProps<TTimeline, TManageItem>,
- 'CardComponent'
- >
- >;
- dataVersion?: number;
- notFoundI18nKey: string;
-}
-
-export default function TimelinePageTemplate<
- TManageItem,
- TTimeline extends TimelineInfo
->(
- props: TimelinePageTemplateProps<TManageItem, TTimeline>
-): React.ReactElement | null {
- const { t } = useTranslation();
-
- const { name } = props;
-
- const user = useUser();
-
- const [dialog, setDialog] = React.useState<null | 'property' | 'member'>(
- null
- );
- const [timeline, setTimeline] = React.useState<TTimeline | undefined>(
- undefined
- );
- const [posts, setPosts] = React.useState<
- TimelinePostInfoEx[] | 'forbid' | undefined
- >(undefined);
- const [error, setError] = React.useState<string | undefined>(undefined);
-
- const service = props.service;
-
- React.useEffect(() => {
- let subscribe = true;
- service.fetch(name).then(
- (ti) => {
- if (subscribe) {
- setTimeline(ti);
- if (!service.hasReadPermission(user, ti)) {
- setPosts('forbid');
- } else {
- service.fetchPosts(name).then(
- (data) => {
- if (subscribe) {
- setPosts(
- data.map((post) => ({
- ...post,
- deletable: service.hasModifyPostPermission(
- user,
- ti,
- post
- ),
- }))
- );
- }
- },
- (error) => {
- if (subscribe) {
- setError(`${error as string}`);
- }
- }
- );
- }
- }
- },
- (error: AxiosError) => {
- if (subscribe) {
- if (
- extractStatusCode(error) === 404 ||
- extractErrorCode(error) === 11020101
- ) {
- setError(t(props.notFoundI18nKey));
- } else {
- setError(error.toString());
- }
- }
- }
- );
- return () => {
- subscribe = false;
- };
- }, [name, service, user, t, props.dataVersion, props.notFoundI18nKey]);
-
- const closeDialog = React.useCallback((): void => {
- setDialog(null);
- }, []);
-
- let dialogElement: React.ReactElement | undefined;
-
- if (dialog === 'property') {
- if (timeline == null) {
- throw new UiLogicError(
- 'Timeline is null but attempt to open change property dialog.'
- );
- }
-
- dialogElement = (
- <TimelinePropertyChangeDialog
- open
- close={closeDialog}
- oldInfo={{
- visibility: timeline.visibility,
- description: timeline.description,
- }}
- onProcess={(req) => {
- return service.changeProperty(name, req).then((newTimeline) => {
- setTimeline(newTimeline);
- });
- }}
- />
- );
- } else if (dialog === 'member') {
- if (timeline == null) {
- throw new UiLogicError(
- 'Timeline is null but attempt to open change property dialog.'
- );
- }
-
- dialogElement = (
- <TimelineMemberDialog
- open
- onClose={closeDialog}
- members={[timeline.owner, ...timeline.members]}
- edit={
- service.hasManagePermission(user, timeline)
- ? {
- onCheckUser: (u) => {
- return fetchUser(u).catch((e) => {
- if (
- extractStatusCode(e) === 404 ||
- extractErrorCode(e) === 11020101
- ) {
- return Promise.resolve(null);
- } else {
- return Promise.reject(e);
- }
- });
- },
- onAddUser: (u) => {
- return service.addMember(name, u.username).then((_) => {
- setTimeline({
- ...timeline,
- members: concat(timeline.members, u),
- });
- });
- },
- onRemoveUser: (u) => {
- void service.removeMember(name, u).then((_) => {
- setTimeline({
- ...timeline,
- members: without(
- timeline.members,
- timeline.members.find((m) => m.username === u)
- ),
- });
- });
- },
- }
- : null
- }
- />
- );
- }
-
- const { UiComponent } = props;
-
- const onDelete: TimelineDeleteCallback = React.useCallback(
- (index, id) => {
- service.deletePost(name, id).then(
- (_) => {
- setPosts((oldPosts) =>
- without(
- oldPosts as TimelinePostInfoEx[],
- (oldPosts as TimelinePostInfoEx[])[index]
- )
- );
- },
- () => {
- pushAlert({
- type: 'danger',
- message: t('timeline.deletePostFailed'),
- });
- }
- );
- },
- [service, name, t]
- );
-
- const onPost: TimelinePostSendCallback = React.useCallback(
- (req) => {
- return service.createPost(name, req).then((newPost) => {
- setPosts((oldPosts) =>
- concat(oldPosts as TimelinePostInfoEx[], {
- ...newPost,
- deletable: true,
- })
- );
- });
- },
- [service, name]
- );
-
- const onManageProp = props.onManage;
-
- const onManage = React.useCallback(
- (item: 'property' | TManageItem) => {
- if (item === 'property') {
- setDialog(item);
- } else {
- onManageProp(item);
- }
- },
- [onManageProp]
- );
-
- const onMember = React.useCallback(() => {
- setDialog('member');
- }, []);
-
- return (
- <>
- <UiComponent
- error={error}
- timeline={timeline}
- posts={posts}
- onDelete={onDelete}
- onPost={
- timeline != null && service.hasPostPermission(user, timeline)
- ? onPost
- : undefined
- }
- onManage={
- timeline != null && service.hasManagePermission(user, timeline)
- ? onManage
- : undefined
- }
- onMember={onMember}
- />
- {dialogElement}
- </>
- );
-}
diff --git a/Timeline/ClientApp/src/app/timeline/TimelinePageTemplateUI.tsx b/Timeline/ClientApp/src/app/timeline/TimelinePageTemplateUI.tsx
deleted file mode 100644
index 924e7883..00000000
--- a/Timeline/ClientApp/src/app/timeline/TimelinePageTemplateUI.tsx
+++ /dev/null
@@ -1,243 +0,0 @@
-import React from 'react';
-import { Spinner } from 'reactstrap';
-import { useTranslation } from 'react-i18next';
-import { fromEvent } from 'rxjs';
-import Svg from 'react-inlinesvg';
-
-import arrowsAngleContractIcon from 'bootstrap-icons/icons/arrows-angle-contract.svg';
-import arrowsAngleExpandIcon from 'bootstrap-icons/icons/arrows-angle-expand.svg';
-
-import { getAlertHost } from '../common/alert-service';
-
-import Timeline, {
- TimelinePostInfoEx,
- TimelineDeleteCallback,
-} from './Timeline';
-import AppBar from '../common/AppBar';
-import TimelinePostEdit, { TimelinePostSendCallback } from './TimelinePostEdit';
-import { useEventEmiiter } from '../common';
-
-export interface TimelineCardComponentProps<TTimeline, TManageItems> {
- timeline: TTimeline;
- onManage?: (item: TManageItems | 'property') => void;
- onMember: () => void;
- className?: string;
- onHeight?: (height: number) => void;
-}
-
-export interface TimelinePageTemplateUIProps<
- TTimeline extends { uniqueId: string },
- TManageItems
-> {
- avatarKey?: string | number;
- timeline?: TTimeline;
- posts?: TimelinePostInfoEx[] | 'forbid';
- CardComponent: React.ComponentType<
- TimelineCardComponentProps<TTimeline, TManageItems>
- >;
- onMember: () => void;
- onManage?: (item: TManageItems | 'property') => void;
- onPost?: TimelinePostSendCallback;
- onDelete: TimelineDeleteCallback;
- error?: string;
-}
-
-export default function TimelinePageTemplateUI<
- TTimeline extends { uniqueId: string },
- TEditItems
->(
- props: TimelinePageTemplateUIProps<TTimeline, TEditItems>
-): React.ReactElement | null {
- const { timeline } = props;
-
- const { t } = useTranslation();
-
- const bottomSpaceRef = React.useRef<HTMLDivElement | null>(null);
-
- const onPostEditHeightChange = React.useCallback((height: number): void => {
- const { current: bottomSpaceDiv } = bottomSpaceRef;
- if (bottomSpaceDiv != null) {
- bottomSpaceDiv.style.height = `${height}px`;
- }
- if (height === 0) {
- const alertHost = getAlertHost();
- if (alertHost != null) {
- alertHost.style.removeProperty('margin-bottom');
- }
- } else {
- const alertHost = getAlertHost();
- if (alertHost != null) {
- alertHost.style.marginBottom = `${height}px`;
- }
- }
- }, []);
-
- const timelineRef = React.useRef<HTMLDivElement | null>(null);
-
- const [getResizeEvent, triggerResizeEvent] = useEventEmiiter();
-
- React.useEffect(() => {
- const { current: timelineElement } = timelineRef;
- if (timelineElement != null) {
- let loadingScrollToBottom = true;
- let pinBottom = false;
-
- const isAtBottom = (): boolean =>
- window.innerHeight + window.scrollY + 10 >= document.body.scrollHeight;
-
- const disableLoadingScrollToBottom = (): void => {
- loadingScrollToBottom = false;
- if (isAtBottom()) pinBottom = true;
- };
-
- const checkAndScrollToBottom = (): void => {
- if (loadingScrollToBottom || pinBottom) {
- window.scrollTo(0, document.body.scrollHeight);
- }
- };
-
- const subscriptions = [
- fromEvent(timelineElement, 'wheel').subscribe(
- disableLoadingScrollToBottom
- ),
- fromEvent(timelineElement, 'pointerdown').subscribe(
- disableLoadingScrollToBottom
- ),
- fromEvent(timelineElement, 'keydown').subscribe(
- disableLoadingScrollToBottom
- ),
- fromEvent(window, 'scroll').subscribe(() => {
- if (loadingScrollToBottom) return;
-
- if (isAtBottom()) {
- pinBottom = true;
- } else {
- pinBottom = false;
- }
- }),
- fromEvent(window, 'resize').subscribe(checkAndScrollToBottom),
- getResizeEvent().subscribe(checkAndScrollToBottom),
- ];
-
- return () => {
- subscriptions.forEach((s) => s.unsubscribe());
- };
- }
- }, [getResizeEvent, triggerResizeEvent, timeline, props.posts]);
-
- const [cardHeight, setCardHeight] = React.useState<number>(0);
-
- const genCardCollapseLocalStorageKey = (uniqueId: string): string =>
- `timeline.${uniqueId}.cardCollapse`;
-
- const cardCollapseLocalStorageKey =
- timeline != null ? genCardCollapseLocalStorageKey(timeline.uniqueId) : null;
-
- const [infoCardCollapse, setInfoCardCollapse] = React.useState<boolean>(true);
- React.useEffect(() => {
- if (cardCollapseLocalStorageKey != null) {
- const savedCollapse =
- window.localStorage.getItem(cardCollapseLocalStorageKey) === 'true';
- setInfoCardCollapse(savedCollapse);
- }
- }, [cardCollapseLocalStorageKey]);
-
- let body: React.ReactElement;
-
- if (props.error != null) {
- body = <p className="text-danger">{t(props.error)}</p>;
- } else {
- if (timeline != null) {
- let timelineBody: React.ReactElement;
- if (props.posts != null) {
- if (props.posts === 'forbid') {
- timelineBody = (
- <p className="text-danger">{t('timeline.messageCantSee')}</p>
- );
- } else {
- timelineBody = (
- <Timeline
- containerRef={timelineRef}
- posts={props.posts}
- onDelete={props.onDelete}
- onResize={triggerResizeEvent}
- />
- );
- if (props.onPost != null) {
- timelineBody = (
- <>
- {timelineBody}
- <div ref={bottomSpaceRef} className="flex-fix-length" />
- <TimelinePostEdit
- onPost={props.onPost}
- onHeightChange={onPostEditHeightChange}
- timelineUniqueId={timeline.uniqueId}
- />
- </>
- );
- }
- }
- } else {
- timelineBody = (
- <div className="full-viewport-center-child">
- <Spinner color="primary" type="grow" />
- </div>
- );
- }
- const { CardComponent } = props;
-
- body = (
- <>
- <div
- className="fixed-top mt-appbar info-card-container"
- data-collapse={infoCardCollapse ? 'true' : 'false'}
- >
- <Svg
- src={
- infoCardCollapse
- ? arrowsAngleExpandIcon
- : arrowsAngleContractIcon
- }
- onClick={() => {
- const newState = !infoCardCollapse;
- setInfoCardCollapse(newState);
- window.localStorage.setItem(
- genCardCollapseLocalStorageKey(timeline.uniqueId),
- newState.toString()
- );
- }}
- className="float-right m-1 info-card-collapse-button text-primary icon-button"
- />
- <CardComponent
- timeline={timeline}
- onManage={props.onManage}
- onMember={props.onMember}
- onHeight={setCardHeight}
- className="info-card-content"
- />
- </div>
- {timelineBody}
- </>
- );
- } else {
- body = (
- <div className="full-viewport-center-child">
- <Spinner color="primary" type="grow" />
- </div>
- );
- }
- }
-
- return (
- <>
- <AppBar />
- <div>
- <div
- style={{ height: 56 + cardHeight }}
- className="timeline-page-top-space flex-fix-length"
- />
- {body}
- </div>
- </>
- );
-}
diff --git a/Timeline/ClientApp/src/app/timeline/TimelinePageUI.tsx b/Timeline/ClientApp/src/app/timeline/TimelinePageUI.tsx
deleted file mode 100644
index d7e9d81b..00000000
--- a/Timeline/ClientApp/src/app/timeline/TimelinePageUI.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import React from 'react';
-
-import { ExcludeKey } from '../type-utilities';
-import { TimelineInfo } from '../data/timeline';
-
-import TimelinePageTemplateUI, {
- TimelinePageTemplateUIProps
-} from './TimelinePageTemplateUI';
-import TimelineInfoCard, {
- OrdinaryTimelineManageItem
-} from './TimelineInfoCard';
-
-export type TimelinePageUIProps = ExcludeKey<
- TimelinePageTemplateUIProps<TimelineInfo, OrdinaryTimelineManageItem>,
- 'CardComponent'
->;
-
-const TimelinePageUI: React.FC<TimelinePageUIProps> = props => {
- return <TimelinePageTemplateUI {...props} CardComponent={TimelineInfoCard} />;
-};
-
-export default TimelinePageUI;
diff --git a/Timeline/ClientApp/src/app/timeline/TimelinePostEdit.tsx b/Timeline/ClientApp/src/app/timeline/TimelinePostEdit.tsx
deleted file mode 100644
index 894d6ad4..00000000
--- a/Timeline/ClientApp/src/app/timeline/TimelinePostEdit.tsx
+++ /dev/null
@@ -1,234 +0,0 @@
-import React from 'react';
-import { Button, Spinner, Row, Col } from 'reactstrap';
-import { useTranslation } from 'react-i18next';
-import Svg from 'react-inlinesvg';
-
-import textIcon from 'bootstrap-icons/icons/card-text.svg';
-import imageIcon from 'bootstrap-icons/icons/image.svg';
-
-import { pushAlert } from '../common/alert-service';
-import { CreatePostRequest } from '../data/timeline';
-
-import FileInput from '../common/FileInput';
-import { UiLogicError } from '../common';
-
-interface TimelinePostEditImageProps {
- onSelect: (blob: Blob | null) => void;
-}
-
-const TimelinePostEditImage: React.FC<TimelinePostEditImageProps> = (props) => {
- const { onSelect } = props;
- const { t } = useTranslation();
-
- const [file, setFile] = React.useState<File | null>(null);
- const [fileUrl, setFileUrl] = React.useState<string | null>(null);
- const [error, setError] = React.useState<string | null>(null);
-
- React.useEffect(() => {
- if (file != null) {
- const url = URL.createObjectURL(file);
- setFileUrl(url);
- return () => {
- URL.revokeObjectURL(url);
- };
- }
- }, [file]);
-
- const onInputChange: React.ChangeEventHandler<HTMLInputElement> = React.useCallback(
- (e) => {
- const files = e.target.files;
- if (files == null || files.length === 0) {
- setFile(null);
- setFileUrl(null);
- } else {
- setFile(files[0]);
- }
- onSelect(null);
- setError(null);
- },
- [onSelect]
- );
-
- const onImgLoad = React.useCallback(() => {
- onSelect(file);
- }, [onSelect, file]);
-
- const onImgError = React.useCallback(() => {
- setError('loadImageError');
- }, []);
-
- return (
- <>
- <FileInput
- labelText={t('chooseImage')}
- onChange={onInputChange}
- accept="image/*"
- className="mx-3 my-1"
- />
- {fileUrl && error == null && (
- <img
- src={fileUrl}
- className="timeline-post-edit-image"
- onLoad={onImgLoad}
- onError={onImgError}
- />
- )}
- {error != null && <div className="text-danger">{t(error)}</div>}
- </>
- );
-};
-
-export type TimelinePostSendCallback = (
- content: CreatePostRequest
-) => Promise<void>;
-
-export interface TimelinePostEditProps {
- className?: string;
- onPost: TimelinePostSendCallback;
- onHeightChange?: (height: number) => void;
- timelineUniqueId: string;
-}
-
-const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => {
- const { onPost } = props;
-
- const { t } = useTranslation();
-
- const [state, setState] = React.useState<'input' | 'process'>('input');
- const [kind, setKind] = React.useState<'text' | 'image'>('text');
- const [text, setText] = React.useState<string>('');
- const [imageBlob, setImageBlob] = React.useState<Blob | null>(null);
-
- const draftLocalStorageKey = `timeline.${props.timelineUniqueId}.postDraft`;
-
- React.useEffect(() => {
- setText(window.localStorage.getItem(draftLocalStorageKey) ?? '');
- }, [draftLocalStorageKey]);
-
- const canSend = kind === 'text' || (kind === 'image' && imageBlob != null);
-
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- const containerRef = React.useRef<HTMLDivElement>(null!);
-
- const notifyHeightChange = (): void => {
- if (props.onHeightChange) {
- props.onHeightChange(containerRef.current.clientHeight);
- }
- };
-
- React.useEffect(() => {
- if (props.onHeightChange) {
- props.onHeightChange(containerRef.current.clientHeight);
- }
- return () => {
- if (props.onHeightChange) {
- props.onHeightChange(0);
- }
- };
- });
-
- const toggleKind = React.useCallback(() => {
- setKind((oldKind) => (oldKind === 'text' ? 'image' : 'text'));
- setImageBlob(null);
- }, []);
-
- const onSend = React.useCallback(() => {
- setState('process');
-
- const req: CreatePostRequest = (() => {
- switch (kind) {
- case 'text':
- return {
- content: {
- type: 'text',
- text: text,
- },
- } as CreatePostRequest;
- case 'image':
- if (imageBlob == null) {
- throw new UiLogicError(
- 'Content type is image but image blob is null.'
- );
- }
- return {
- content: {
- type: 'image',
- data: imageBlob,
- },
- } as CreatePostRequest;
- default:
- throw new UiLogicError('Unknown content type.');
- }
- })();
-
- onPost(req).then(
- (_) => {
- if (kind === 'text') {
- setText('');
- window.localStorage.removeItem(draftLocalStorageKey);
- }
- setState('input');
- setKind('text');
- },
- (_) => {
- pushAlert({
- type: 'danger',
- message: t('timeline.sendPostFailed'),
- });
- setState('input');
- }
- );
- }, [onPost, kind, text, imageBlob, t, draftLocalStorageKey]);
-
- const onImageSelect = React.useCallback((blob: Blob | null) => {
- setImageBlob(blob);
- }, []);
-
- return (
- <div ref={containerRef} className="container-fluid fixed-bottom bg-light">
- <Row>
- <Col className="px-1 py-1">
- {kind === 'text' ? (
- <textarea
- className="w-100 h-100 timeline-post-edit"
- value={text}
- disabled={state === 'process'}
- onChange={(event: React.ChangeEvent<HTMLTextAreaElement>) => {
- const value = event.currentTarget.value;
- setText(value);
- window.localStorage.setItem(draftLocalStorageKey, value);
- }}
- />
- ) : (
- <TimelinePostEditImage onSelect={onImageSelect} />
- )}
- </Col>
- <Col sm="col-auto align-self-end m-1">
- {(() => {
- if (state === 'input') {
- return (
- <>
- <div className="d-block text-center mt-1 mb-2">
- <Svg
- onLoad={notifyHeightChange}
- src={kind === 'text' ? imageIcon : textIcon}
- className="icon-button"
- onClick={toggleKind}
- />
- </div>
- <Button color="primary" onClick={onSend} disabled={!canSend}>
- {t('timeline.send')}
- </Button>
- </>
- );
- } else {
- return <Spinner />;
- }
- })()}
- </Col>
- </Row>
- </div>
- );
-};
-
-export default TimelinePostEdit;
diff --git a/Timeline/ClientApp/src/app/timeline/TimelinePropertyChangeDialog.tsx b/Timeline/ClientApp/src/app/timeline/TimelinePropertyChangeDialog.tsx
deleted file mode 100644
index ca1be31c..00000000
--- a/Timeline/ClientApp/src/app/timeline/TimelinePropertyChangeDialog.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-import React from 'react';
-
-import {
- TimelineVisibility,
- kTimelineVisibilities,
- PersonalTimelineChangePropertyRequest
-} from '../data/timeline';
-
-import OperationDialog, {
- OperationSelectInputInfoOption
-} from '../common/OperationDialog';
-
-export interface TimelinePropertyInfo {
- visibility: TimelineVisibility;
- description: string;
-}
-
-export interface TimelinePropertyChangeDialogProps {
- open: boolean;
- close: () => void;
- oldInfo: TimelinePropertyInfo;
- onProcess: (request: PersonalTimelineChangePropertyRequest) => Promise<void>;
-}
-
-const labelMap: { [key in TimelineVisibility]: string } = {
- Private: 'timeline.visibility.private',
- Public: 'timeline.visibility.public',
- Register: 'timeline.visibility.register'
-};
-
-const TimelinePropertyChangeDialog: React.FC<TimelinePropertyChangeDialogProps> = props => {
- return (
- <OperationDialog
- title={'timeline.dialogChangeProperty.title'}
- titleColor="default"
- inputScheme={[
- {
- type: 'select',
- label: 'timeline.dialogChangeProperty.visibility',
- options: kTimelineVisibilities.map<OperationSelectInputInfoOption>(
- v => ({
- label: labelMap[v],
- value: v
- })
- ),
- initValue: props.oldInfo.visibility
- },
- {
- type: 'text',
- label: 'timeline.dialogChangeProperty.description',
- initValue: props.oldInfo.description
- }
- ]}
- open={props.open}
- close={props.close}
- onProcess={([newVisibility, newDescription]) => {
- const req: PersonalTimelineChangePropertyRequest = {};
- if (newVisibility !== props.oldInfo.visibility) {
- req.visibility = newVisibility as TimelineVisibility;
- }
- if (newDescription !== props.oldInfo.description) {
- req.description = newDescription as string;
- }
- return props.onProcess(req);
- }}
- />
- );
-};
-
-export default TimelinePropertyChangeDialog;
diff --git a/Timeline/ClientApp/src/app/timeline/timeline-ui.sass b/Timeline/ClientApp/src/app/timeline/timeline-ui.sass
deleted file mode 100644
index c3616caf..00000000
--- a/Timeline/ClientApp/src/app/timeline/timeline-ui.sass
+++ /dev/null
@@ -1,18 +0,0 @@
-.info-card-container
- .info-card-collapse-button
- z-index: 1
- position: relative
-
- .info-card-content
- width: 100%
- position: absolute
- transform-origin: right top
- transition: transform 0.5s
-
- &[data-collapse='true']
- .info-card-content
- transform: scale(0)
-
-.timeline-page-top-space
- transition: height 0.5s
-
diff --git a/Timeline/ClientApp/src/app/timeline/timeline.sass b/Timeline/ClientApp/src/app/timeline/timeline.sass
deleted file mode 100644
index 0b0e73b5..00000000
--- a/Timeline/ClientApp/src/app/timeline/timeline.sass
+++ /dev/null
@@ -1,125 +0,0 @@
-@use 'sass:color'
-
-@keyframes timeline-enter-animation-mask-animation
- to
- height: 0
-
-.timeline-enter-animation-mask
- position: absolute
- left: 0
- top: 0
- height: calc(100% + 300px)
- width: 100%
- background: linear-gradient(to top, #ffffff00 0, 200px, white 300px, white)
- z-index: 100
- animation: timeline-enter-animation-mask-animation 5s 0.3s forwards // Give it 0.3s to load, which I think is reasonable
-
-$timeline-line-width: 7px
-$timeline-line-node-radius: 18px
-$timeline-line-color: $primary
-$timeline-line-color-current: #36c2e6
-
-@keyframes timeline-line-node-noncurrent
- from
- background: $timeline-line-color
-
- to
- background: color.adjust($timeline-line-color, $lightness: +10%)
- box-shadow: 0 0 20px 3px color.adjust($timeline-line-color, $lightness: +10%, $alpha: -0.1)
-
-
-@keyframes timeline-line-node-current
- from
- background: $timeline-line-color-current
-
- to
- background: color.adjust($timeline-line-color-current, $lightness: +10%)
- box-shadow: 0 0 20px 3px color.adjust($timeline-line-color-current, $lightness: +10%, $alpha: -0.1)
-
-.timeline-line
- &-area
- display: flex
- flex-direction: column
- align-items: center
- flex: 0 0 auto
- width: 60px
-
- &-segment
- width: $timeline-line-width
- background: $timeline-line-color
-
- &.start
- height: 20px
- flex: 0 0 auto
-
- &.end
- flex: 1 1 auto
-
- &.current-end
- height: 20px
- flex: 0 0 auto
- background: linear-gradient($timeline-line-color-current, transparent)
-
- &-node-container
- flex: 0 0 auto
- position: relative
- width: $timeline-line-node-radius
- height: $timeline-line-node-radius
-
- &-node
- width: $timeline-line-node-radius + 2
- height: $timeline-line-node-radius + 2
- position: absolute
- left: -1px
- top: -1px
- border-radius: 50%
- box-sizing: border-box
- z-index: 1
- animation: 1s infinite alternate
- animation-name: timeline-line-node-noncurrent
-
-
-.current
- .timeline-line
- &-segment
-
- &.start
- background: linear-gradient($timeline-line-color, $timeline-line-color-current)
-
- &.end
- background: $timeline-line-color-current
-
- &-node
- animation-name: timeline-line-node-current
-
-.timeline-pt-start
- padding-top: 18px
-
-.timeline-item-delete-button
- position: absolute
- right: 0
- bottom: 0
-
-.timeline-content
- white-space: pre-line
-
-.timeline-content-image
- max-width: 60%
- max-height: 200px
-
-
-.timeline-post-edit-image
- max-width: 100px
- max-height: 100px
-
-.mask
- background: change-color($color: white, $alpha: 0.8)
- z-index: 100
-
-textarea.timeline-post-edit
- @extend .border-primary
- @extend .rounded
-
- &:focus
- outline: none
- box-shadow: 0 0 5px 0 $primary
diff --git a/Timeline/ClientApp/src/app/tsconfig.json b/Timeline/ClientApp/src/app/tsconfig.json
deleted file mode 100644
index 14e6327f..00000000
--- a/Timeline/ClientApp/src/app/tsconfig.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "extends": "../tsconfig.json",
- "compilerOptions": {
- "lib": [
- "dom",
- "dom.iterable",
- "esnext"
- ]
- },
- "include": [
- "."
- ]
-}
diff --git a/Timeline/ClientApp/src/app/type-utilities.ts b/Timeline/ClientApp/src/app/type-utilities.ts
deleted file mode 100644
index 8df9bf0f..00000000
--- a/Timeline/ClientApp/src/app/type-utilities.ts
+++ /dev/null
@@ -1 +0,0 @@
-export type ExcludeKey<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
diff --git a/Timeline/ClientApp/src/app/typings.d.ts b/Timeline/ClientApp/src/app/typings.d.ts
deleted file mode 100644
index 426fb214..00000000
--- a/Timeline/ClientApp/src/app/typings.d.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-declare module '*.png' {
- const content: string;
- export default content;
-}
-
-declare module '*.jpeg' {
- const content: string;
- export default content;
-}
-
-declare module '*.jpg' {
- const content: string;
- export default content;
-}
-
-declare module '*.gif' {
- const content: string;
- export default content;
-}
-
-declare module '*.svg' {
- const content: string;
- export default content;
-}
diff --git a/Timeline/ClientApp/src/app/user/ChangeAvatarDialog.tsx b/Timeline/ClientApp/src/app/user/ChangeAvatarDialog.tsx
deleted file mode 100644
index a0e56621..00000000
--- a/Timeline/ClientApp/src/app/user/ChangeAvatarDialog.tsx
+++ /dev/null
@@ -1,306 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { useTranslation } from 'react-i18next';
-import {
- Modal,
- ModalHeader,
- Row,
- Button,
- ModalBody,
- ModalFooter,
-} from 'reactstrap';
-import { AxiosError } from 'axios';
-
-import ImageCropper, { Clip, applyClipToImage } from '../common/ImageCropper';
-import { UiLogicError } from '../common';
-
-export interface ChangeAvatarDialogProps {
- open: boolean;
- close: () => void;
- process: (blob: Blob) => Promise<void>;
-}
-
-const ChangeAvatarDialog: React.FC<ChangeAvatarDialogProps> = (props) => {
- const { t } = useTranslation();
-
- const [file, setFile] = React.useState<File | null>(null);
- const [fileUrl, setFileUrl] = React.useState<string | null>(null);
- const [clip, setClip] = React.useState<Clip | null>(null);
- const [
- cropImgElement,
- setCropImgElement,
- ] = React.useState<HTMLImageElement | null>(null);
- const [resultBlob, setResultBlob] = React.useState<Blob | null>(null);
- const [resultUrl, setResultUrl] = React.useState<string | null>(null);
-
- const [state, setState] = React.useState<
- | 'select'
- | 'crop'
- | 'processcrop'
- | 'preview'
- | 'uploading'
- | 'success'
- | 'error'
- >('select');
-
- const [message, setMessage] = useState<
- string | { type: 'custom'; text: string } | null
- >('userPage.dialogChangeAvatar.prompt.select');
-
- const trueMessage =
- message == null
- ? null
- : typeof message === 'string'
- ? t(message)
- : message.text;
-
- const closeDialog = props.close;
-
- const toggle = React.useCallback((): void => {
- if (!(state === 'uploading')) {
- closeDialog();
- }
- }, [state, closeDialog]);
-
- useEffect(() => {
- if (file != null) {
- const url = URL.createObjectURL(file);
- setClip(null);
- setFileUrl(url);
- setState('crop');
- return () => {
- URL.revokeObjectURL(url);
- };
- } else {
- setFileUrl(null);
- setState('select');
- }
- }, [file]);
-
- React.useEffect(() => {
- if (resultBlob != null) {
- const url = URL.createObjectURL(resultBlob);
- setResultUrl(url);
- setState('preview');
- return () => {
- URL.revokeObjectURL(url);
- };
- } else {
- setResultUrl(null);
- }
- }, [resultBlob]);
-
- const onSelectFile = React.useCallback(
- (e: React.ChangeEvent<HTMLInputElement>): void => {
- const files = e.target.files;
- if (files == null || files.length === 0) {
- setFile(null);
- } else {
- setFile(files[0]);
- }
- },
- []
- );
-
- const onCropNext = React.useCallback(() => {
- if (
- cropImgElement == null ||
- clip == null ||
- clip.width === 0 ||
- file == null
- ) {
- throw new UiLogicError();
- }
-
- setState('processcrop');
- void applyClipToImage(cropImgElement, clip, file.type).then((b) => {
- setResultBlob(b);
- });
- }, [cropImgElement, clip, file]);
-
- const onCropPrevious = React.useCallback(() => {
- setFile(null);
- setState('select');
- }, []);
-
- const onPreviewPrevious = React.useCallback(() => {
- setResultBlob(null);
- setState('crop');
- }, []);
-
- const process = props.process;
-
- const upload = React.useCallback(() => {
- if (resultBlob == null) {
- throw new UiLogicError();
- }
-
- setState('uploading');
- process(resultBlob).then(
- () => {
- setState('success');
- },
- (e: unknown) => {
- setState('error');
- setMessage({ type: 'custom', text: (e as AxiosError).message });
- }
- );
- }, [resultBlob, process]);
-
- const createPreviewRow = (): React.ReactElement => {
- if (resultUrl == null) {
- throw new UiLogicError();
- }
- return (
- <Row className="justify-content-center">
- <img
- className="change-avatar-img"
- src={resultUrl}
- alt={t('userPage.dialogChangeAvatar.previewImgAlt')}
- />
- </Row>
- );
- };
-
- return (
- <Modal isOpen={props.open} toggle={toggle}>
- <ModalHeader> {t('userPage.dialogChangeAvatar.title')}</ModalHeader>
- {(() => {
- if (state === 'select') {
- return (
- <>
- <ModalBody className="container">
- <Row>{t('userPage.dialogChangeAvatar.prompt.select')}</Row>
- <Row>
- <input type="file" accept="image/*" onChange={onSelectFile} />
- </Row>
- </ModalBody>
- <ModalFooter>
- <Button color="secondary" onClick={toggle}>
- {t('operationDialog.cancel')}
- </Button>
- </ModalFooter>
- </>
- );
- } else if (state === 'crop') {
- if (fileUrl == null) {
- throw new UiLogicError();
- }
- return (
- <>
- <ModalBody className="container">
- <Row className="justify-content-center">
- <ImageCropper
- clip={clip}
- onChange={setClip}
- imageUrl={fileUrl}
- imageElementCallback={setCropImgElement}
- />
- </Row>
- <Row>{t('userPage.dialogChangeAvatar.prompt.crop')}</Row>
- </ModalBody>
- <ModalFooter>
- <Button color="secondary" onClick={toggle}>
- {t('operationDialog.cancel')}
- </Button>
- <Button color="secondary" onClick={onCropPrevious}>
- {t('operationDialog.previousStep')}
- </Button>
- <Button
- color="primary"
- onClick={onCropNext}
- disabled={
- cropImgElement == null || clip == null || clip.width === 0
- }
- >
- {t('operationDialog.nextStep')}
- </Button>
- </ModalFooter>
- </>
- );
- } else if (state === 'processcrop') {
- return (
- <>
- <ModalBody className="container">
- <Row>
- {t('userPage.dialogChangeAvatar.prompt.processingCrop')}
- </Row>
- </ModalBody>
- <ModalFooter>
- <Button color="secondary" onClick={toggle}>
- {t('operationDialog.cancel')}
- </Button>
- <Button color="secondary" onClick={onPreviewPrevious}>
- {t('operationDialog.previousStep')}
- </Button>
- </ModalFooter>
- </>
- );
- } else if (state === 'preview') {
- return (
- <>
- <ModalBody className="container">
- {createPreviewRow()}
- <Row>{t('userPage.dialogChangeAvatar.prompt.preview')}</Row>
- </ModalBody>
- <ModalFooter>
- <Button color="secondary" onClick={toggle}>
- {t('operationDialog.cancel')}
- </Button>
- <Button color="secondary" onClick={onPreviewPrevious}>
- {t('operationDialog.previousStep')}
- </Button>
- <Button color="primary" onClick={upload}>
- {t('userPage.dialogChangeAvatar.upload')}
- </Button>
- </ModalFooter>
- </>
- );
- } else if (state === 'uploading') {
- return (
- <>
- <ModalBody className="container">
- {createPreviewRow()}
- <Row>{t('userPage.dialogChangeAvatar.prompt.uploading')}</Row>
- </ModalBody>
- <ModalFooter></ModalFooter>
- </>
- );
- } else if (state === 'success') {
- return (
- <>
- <ModalBody className="container">
- <Row className="p-4 text-success">
- {t('operationDialog.success')}
- </Row>
- </ModalBody>
- <ModalFooter>
- <Button color="success" onClick={toggle}>
- {t('operationDialog.ok')}
- </Button>
- </ModalFooter>
- </>
- );
- } else {
- return (
- <>
- <ModalBody className="container">
- {createPreviewRow()}
- <Row className="text-danger">{trueMessage}</Row>
- </ModalBody>
- <ModalFooter>
- <Button color="secondary" onClick={toggle}>
- {t('operationDialog.cancel')}
- </Button>
- <Button color="primary" onClick={upload}>
- {t('operationDialog.retry')}
- </Button>
- </ModalFooter>
- </>
- );
- }
- })()}
- </Modal>
- );
-};
-
-export default ChangeAvatarDialog;
diff --git a/Timeline/ClientApp/src/app/user/ChangeNicknameDialog.tsx b/Timeline/ClientApp/src/app/user/ChangeNicknameDialog.tsx
deleted file mode 100644
index dbc216e1..00000000
--- a/Timeline/ClientApp/src/app/user/ChangeNicknameDialog.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import React from 'react';
-
-import OperationDialog from '../common/OperationDialog';
-
-export interface ChangeNicknameDialogProps {
- open: boolean;
- close: () => void;
- onProcess: (newNickname: string) => Promise<void>;
-}
-
-const ChangeNicknameDialog: React.FC<ChangeNicknameDialogProps> = props => {
- return (
- <OperationDialog
- open={props.open}
- title="userPage.dialogChangeNickname.title"
- titleColor="default"
- inputScheme={[
- { type: 'text', label: 'userPage.dialogChangeNickname.inputLabel' }
- ]}
- onProcess={([newNickname]) => {
- return props.onProcess(newNickname as string);
- }}
- close={props.close}
- />
- );
-};
-
-export default ChangeNicknameDialog;
diff --git a/Timeline/ClientApp/src/app/user/Login.tsx b/Timeline/ClientApp/src/app/user/Login.tsx
deleted file mode 100644
index f8b3f0e7..00000000
--- a/Timeline/ClientApp/src/app/user/Login.tsx
+++ /dev/null
@@ -1,147 +0,0 @@
-import React, { Fragment, useState, useEffect } from 'react';
-import { useHistory } from 'react-router';
-import { useTranslation } from 'react-i18next';
-import { AxiosError } from 'axios';
-
-import AppBar from '../common/AppBar';
-
-import { userLogin, useUser } from '../data/user';
-import {
- Label,
- FormGroup,
- Input,
- Form,
- FormFeedback,
- Spinner,
- Button,
-} from 'reactstrap';
-
-const Login: React.FC = (_) => {
- const { t } = useTranslation();
- const history = useHistory();
- const [username, setUsername] = useState<string>('');
- const [usernameDirty, setUsernameDirty] = useState<boolean>(false);
- const [password, setPassword] = useState<string>('');
- const [passwordDirty, setPasswordDirty] = useState<boolean>(false);
- const [rememberMe, setRememberMe] = useState<boolean>(true);
- const [process, setProcess] = useState<boolean>(false);
- const [error, setError] = useState<string | null>(null);
-
- const user = useUser();
-
- useEffect(() => {
- if (user != null) {
- const id = setTimeout(() => history.push('/'), 3000);
- return () => {
- clearTimeout(id);
- };
- }
- }, [history, user]);
-
- if (user != null) {
- return (
- <>
- <AppBar />
- <p className="mt-appbar">{t('login.alreadyLogin')}</p>
- </>
- );
- }
-
- function onSubmit(event: React.SyntheticEvent): void {
- if (username === '' || password === '') {
- setUsernameDirty(true);
- setPasswordDirty(true);
- return;
- }
-
- setProcess(true);
- userLogin(
- {
- username: username,
- password: password,
- },
- rememberMe
- ).then(
- (_) => {
- if (history.length === 0) {
- history.push('/');
- } else {
- history.goBack();
- }
- },
- (e: AxiosError | Error) => {
- setProcess(false);
- setError(e.message);
- }
- );
- event.preventDefault();
- }
-
- return (
- <Fragment>
- <AppBar />
- <div className="container login-container mt-appbar">
- <h1>{t('welcome')}</h1>
- <Form>
- <FormGroup>
- <Label for="username">{t('user.username')}</Label>
- <Input
- id="username"
- disabled={process}
- onChange={(e) => {
- setUsername(e.target.value);
- setUsernameDirty(true);
- }}
- value={username}
- invalid={usernameDirty && username === ''}
- />
- {usernameDirty && username === '' && (
- <FormFeedback>{t('login.emptyUsername')}</FormFeedback>
- )}
- </FormGroup>
- <FormGroup>
- <Label for="password">{t('user.password')}</Label>
- <Input
- id="password"
- type="password"
- disabled={process}
- onChange={(e) => {
- setPassword(e.target.value);
- setPasswordDirty(true);
- }}
- value={password}
- invalid={passwordDirty && password === ''}
- />
- {passwordDirty && password === '' && (
- <FormFeedback>{t('login.emptyPassword')}</FormFeedback>
- )}
- </FormGroup>
- <FormGroup check>
- <Input
- id="remember-me"
- type="checkbox"
- checked={rememberMe}
- onChange={(e) => {
- const v = (e.target as HTMLInputElement).checked;
- setRememberMe(v);
- }}
- />
- <Label for="remember-me">{t('user.rememberMe')}</Label>
- </FormGroup>
- {error ? <p className="text-error">{t(error)}</p> : null}
- <div>
- {process ? (
- <Spinner />
- ) : (
- <Button color="primary" onClick={onSubmit}>
- {t('user.login')}
- </Button>
- )}
- </div>
- </Form>
- </div>
- </Fragment>
- );
-};
-
-export default Login;
diff --git a/Timeline/ClientApp/src/app/user/User.tsx b/Timeline/ClientApp/src/app/user/User.tsx
deleted file mode 100644
index 0e1977b1..00000000
--- a/Timeline/ClientApp/src/app/user/User.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import React, { useState } from 'react';
-import { useParams } from 'react-router';
-
-import { useUser } from '../data/user';
-import { changeNickname, changeAvatar } from './api';
-import { personalTimelineService } from '../data/timeline';
-
-import UserPage from './UserPage';
-import ChangeNicknameDialog from './ChangeNicknameDialog';
-import ChangeAvatarDialog from './ChangeAvatarDialog';
-import TimelinePageTemplate from '../timeline/TimelinePageTemplate';
-import { PersonalTimelineManageItem } from './UserInfoCard';
-import { UiLogicError } from '../common';
-
-const User: React.FC = (_) => {
- const { username } = useParams<{ username: string }>();
-
- const user = useUser();
-
- const [dialog, setDialog] = useState<null | PersonalTimelineManageItem>(null);
- const [dataKey, setDataKey] = useState<number>(0);
-
- let dialogElement: React.ReactElement | undefined;
-
- const closeDialogHandler = (): void => {
- setDialog(null);
- };
-
- if (dialog === 'nickname') {
- if (user == null) {
- throw new UiLogicError('Change nickname without login.');
- }
-
- dialogElement = (
- <ChangeNicknameDialog
- open
- close={closeDialogHandler}
- onProcess={(newNickname) => {
- const p = changeNickname(user.token, username, newNickname);
- return p.then((_) => {
- setDataKey(dataKey + 1);
- });
- }}
- />
- );
- } else if (dialog === 'avatar') {
- if (user == null) {
- throw new UiLogicError('Change avatar without login.');
- }
-
- dialogElement = (
- <ChangeAvatarDialog
- open
- close={closeDialogHandler}
- process={(file) => changeAvatar(user.token, username, file, file.type)}
- />
- );
- }
-
- const onManage = React.useCallback((item: PersonalTimelineManageItem) => {
- setDialog(item);
- }, []);
-
- return (
- <>
- <TimelinePageTemplate
- dataVersion={dataKey}
- name={username}
- UiComponent={UserPage}
- onManage={onManage}
- service={personalTimelineService}
- notFoundI18nKey="timeline.userNotExist"
- />
- {dialogElement}
- </>
- );
-};
-
-export default User;
diff --git a/Timeline/ClientApp/src/app/user/UserInfoCard.tsx b/Timeline/ClientApp/src/app/user/UserInfoCard.tsx
deleted file mode 100644
index b4924a5d..00000000
--- a/Timeline/ClientApp/src/app/user/UserInfoCard.tsx
+++ /dev/null
@@ -1,116 +0,0 @@
-import React from 'react';
-import clsx from 'clsx';
-import {
- Dropdown,
- DropdownToggle,
- DropdownMenu,
- DropdownItem,
- Button,
-} from 'reactstrap';
-import { useTranslation } from 'react-i18next';
-import { fromEvent } from 'rxjs';
-
-import {
- TimelineInfo,
- timelineVisibilityTooltipTranslationMap,
-} from '../data/timeline';
-import { useAvatarVersion, useAvatarUrlWithGivenVersion } from './api';
-import { useUser } from '../data/user';
-
-import { TimelineCardComponentProps } from '../timeline/TimelinePageTemplateUI';
-
-export type PersonalTimelineManageItem = 'avatar' | 'nickname';
-
-export type UserInfoCardProps = TimelineCardComponentProps<
- TimelineInfo,
- PersonalTimelineManageItem
->;
-
-const UserInfoCard: React.FC<UserInfoCardProps> = (props) => {
- const { onHeight, onManage } = props;
- const { t } = useTranslation();
- const user = useUser();
-
- const avatarVersion = useAvatarVersion();
- const avatarUrl = useAvatarUrlWithGivenVersion(
- user != null && user.username === props.timeline.owner.username
- ? avatarVersion
- : undefined,
- props.timeline.owner._links.avatar
- );
-
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- const containerRef = React.useRef<HTMLDivElement>(null!);
-
- const notifyHeight = React.useCallback((): void => {
- if (onHeight) {
- onHeight(containerRef.current.getBoundingClientRect().height);
- }
- }, [onHeight]);
-
- React.useEffect(() => {
- const subscription = fromEvent(window, 'resize').subscribe(notifyHeight);
- return () => subscription.unsubscribe();
- });
-
- const [manageDropdownOpen, setManageDropdownOpen] = React.useState<boolean>(
- false
- );
- const toggleManageDropdown = React.useCallback(
- (): void => setManageDropdownOpen((old) => !old),
- []
- );
-
- return (
- <div
- ref={containerRef}
- className={clsx('rounded border bg-light p-2', props.className)}
- onTransitionEnd={notifyHeight}
- >
- <img
- src={avatarUrl}
- onLoad={notifyHeight}
- className="avatar large mr-2 mb-2 rounded-circle float-left"
- />
- <div>
- {props.timeline.owner.nickname}
- <small className="ml-3 text-secondary">
- @{props.timeline.owner.username}
- </small>
- </div>
- <p className="mb-0">{props.timeline.description}</p>
- <small className="mt-1 d-block">
- {t(timelineVisibilityTooltipTranslationMap[props.timeline.visibility])}
- </small>
- <div className="text-right mt-2">
- {onManage != null ? (
- <Dropdown isOpen={manageDropdownOpen} toggle={toggleManageDropdown}>
- <DropdownToggle outline color="primary">
- {t('timeline.manage')}
- </DropdownToggle>
- <DropdownMenu>
- <DropdownItem onClick={() => onManage('nickname')}>
- {t('timeline.manageItem.nickname')}
- </DropdownItem>
- <DropdownItem onClick={() => onManage('avatar')}>
- {t('timeline.manageItem.avatar')}
- </DropdownItem>
- <DropdownItem onClick={() => onManage('property')}>
- {t('timeline.manageItem.property')}
- </DropdownItem>
- <DropdownItem onClick={props.onMember}>
- {t('timeline.manageItem.member')}
- </DropdownItem>
- </DropdownMenu>
- </Dropdown>
- ) : (
- <Button color="primary" outline onClick={props.onMember}>
- {t('timeline.memberButton')}
- </Button>
- )}
- </div>
- </div>
- );
-};
-
-export default UserInfoCard;
diff --git a/Timeline/ClientApp/src/app/user/UserPage.tsx b/Timeline/ClientApp/src/app/user/UserPage.tsx
deleted file mode 100644
index 98fd06f8..00000000
--- a/Timeline/ClientApp/src/app/user/UserPage.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import React from 'react';
-
-import { ExcludeKey } from '../type-utilities';
-import { TimelineInfo } from '../data/timeline';
-
-import TimelinePageTemplateUI, {
- TimelinePageTemplateUIProps
-} from '../timeline/TimelinePageTemplateUI';
-import UserInfoCard, { PersonalTimelineManageItem } from './UserInfoCard';
-
-export type UserPageProps = ExcludeKey<
- TimelinePageTemplateUIProps<TimelineInfo, PersonalTimelineManageItem>,
- 'CardComponent'
->;
-
-const UserPage: React.FC<UserPageProps> = props => {
- return <TimelinePageTemplateUI {...props} CardComponent={UserInfoCard} />;
-};
-
-export default UserPage;
diff --git a/Timeline/ClientApp/src/app/user/api.ts b/Timeline/ClientApp/src/app/user/api.ts
deleted file mode 100644
index a9855eb0..00000000
--- a/Timeline/ClientApp/src/app/user/api.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-import React from 'react';
-import axios from 'axios';
-import { BehaviorSubject, Observable } from 'rxjs';
-
-import { apiBaseUrl } from '../config';
-import { User } from '../data/user';
-import { updateQueryString } from '../helper';
-
-export function changeNickname(
- token: string,
- username: string,
- newNickname: string
-): Promise<User> {
- return axios
- .patch<User>(`${apiBaseUrl}/users/${username}?token=${token}`, {
- nickname: newNickname,
- })
- .then((res) => res.data);
-}
-
-const avatarVersionSubject = new BehaviorSubject<number | undefined>(undefined);
-
-export function changeAvatar(
- token: string,
- username: string,
- data: Blob,
- type: string
-): Promise<void> {
- return axios
- .put(`${apiBaseUrl}/users/${username}/avatar?token=${token}`, data, {
- headers: {
- 'Content-Type': type,
- },
- })
- .then(() => {
- avatarVersionSubject.next((avatarVersionSubject.value ?? 0) + 1);
- });
-}
-
-export const avatarVersion$: Observable<
- number | undefined
-> = avatarVersionSubject;
-
-export function useAvatarVersion(): number | undefined {
- const [version, setVersion] = React.useState<number | undefined>();
- React.useEffect(() => {
- const subscription = avatarVersion$.subscribe((v) => setVersion(v));
- return () => subscription.unsubscribe();
- }, []);
- return version;
-}
-
-export function useOptionalVersionedAvatarUrl(
- url: string | undefined
-): string | undefined {
- const avatarVersion = useAvatarVersion();
- return React.useMemo(
- () =>
- url == null
- ? undefined
- : updateQueryString(
- 'v',
- avatarVersion == null ? null : avatarVersion.toString(),
- url
- ),
- [avatarVersion, url]
- );
-}
-
-export function useAvatarUrlWithGivenVersion(
- version: number | null | undefined,
- url: string
-): string {
- return React.useMemo(
- () =>
- updateQueryString('v', version == null ? null : version.toString(), url),
- [version, url]
- );
-}
diff --git a/Timeline/ClientApp/src/app/user/user-page.sass b/Timeline/ClientApp/src/app/user/user-page.sass
deleted file mode 100644
index fe3ab70b..00000000
--- a/Timeline/ClientApp/src/app/user/user-page.sass
+++ /dev/null
@@ -1,11 +0,0 @@
-.login-container
- max-width: 600px
-
-.change-avatar-cropper-row
- max-height: 400px
-
-.change-avatar-img
- min-width: 50%
- max-width: 100%
- max-height: 400px
-
diff --git a/Timeline/ClientApp/src/sw/sw.ts b/Timeline/ClientApp/src/sw/sw.ts
deleted file mode 100644
index e7558015..00000000
--- a/Timeline/ClientApp/src/sw/sw.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { precacheAndRoute, matchPrecache } from 'workbox-precaching';
-import { setDefaultHandler } from 'workbox-routing';
-import { NetworkOnly } from 'workbox-strategies';
-
-declare let self: ServiceWorkerGlobalScope;
-
-self.addEventListener('message', (event) => {
- if (event.data && (event.data as { type: string }).type === 'SKIP_WAITING') {
- void self.skipWaiting();
- }
-});
-
-precacheAndRoute(self.__WB_MANIFEST);
-
-const networkOnly = new NetworkOnly();
-
-setDefaultHandler((options) => {
- const { request, url } = options;
- if (url && url.pathname.startsWith('/api/')) {
- return networkOnly.handle(options);
- }
-
- if (request instanceof Request && request.destination === 'document')
- return matchPrecache('/index.html').then((r) =>
- r == null ? Response.error() : r
- );
- else return networkOnly.handle(options);
-});
diff --git a/Timeline/ClientApp/src/sw/tsconfig.json b/Timeline/ClientApp/src/sw/tsconfig.json
deleted file mode 100644
index aac99e59..00000000
--- a/Timeline/ClientApp/src/sw/tsconfig.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "extends": "../tsconfig.json",
- "compilerOptions": {
- "lib": [
- "esnext",
- "webworker"
- ]
- },
- "include": [
- "."
- ]
-}
diff --git a/Timeline/ClientApp/src/tsconfig.json b/Timeline/ClientApp/src/tsconfig.json
deleted file mode 100644
index 320253fa..00000000
--- a/Timeline/ClientApp/src/tsconfig.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
- "compilerOptions": {
- "target": "esnext",
- "allowJs": true,
- "skipLibCheck": true,
- "esModuleInterop": true,
- "allowSyntheticDefaultImports": true,
- "strict": true,
- "forceConsistentCasingInFileNames": true,
- "module": "esnext",
- "moduleResolution": "node",
- "resolveJsonModule": true,
- "isolatedModules": true,
- "noEmit": true,
- "jsx": "react"
- }
-}
diff --git a/Timeline/ClientApp/webpack.common.js b/Timeline/ClientApp/webpack.common.js
deleted file mode 100644
index bed968e1..00000000
--- a/Timeline/ClientApp/webpack.common.js
+++ /dev/null
@@ -1,60 +0,0 @@
-const autoprefixer = require('autoprefixer');
-const htmlWebpackTemplate = require('html-webpack-template');
-
-const commonRules = [
- {
- test: /\.css$/,
- use: ['style-loader', 'css-loader'],
- },
- {
- test: /\.(scss|sass)$/,
- use: [
- 'style-loader',
- 'css-loader',
- {
- loader: 'postcss-loader',
- options: {
- plugins: function () {
- return [autoprefixer];
- },
- },
- },
- 'sass-loader',
- ],
- },
- {
- test: /\.(png|jpe?g|gif|svg|woff|woff2|ttf|eot)$/i,
- use: [
- {
- loader: 'url-loader',
- options: {
- limit: 8192,
- },
- },
- ],
- },
-];
-
-const htmlCommonConfig = {
- inject: false,
- template: htmlWebpackTemplate,
-
- appMountId: 'app',
- mobile: true,
-
- headHtmlSnippet: `
- <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
- <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
- <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
- <link rel="manifest" href="/site.webmanifest">
- <link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
- <meta name="msapplication-TileColor" content="#2d89ef">
- <meta name="theme-color" content="#ffffff">
- `,
- title: 'Timeline',
-};
-
-module.exports = {
- commonRules,
- htmlCommonConfig,
-};
diff --git a/Timeline/ClientApp/webpack.config.dev.js b/Timeline/ClientApp/webpack.config.dev.js
deleted file mode 100644
index c2f29700..00000000
--- a/Timeline/ClientApp/webpack.config.dev.js
+++ /dev/null
@@ -1,78 +0,0 @@
-const path = require('path');
-const webpack = require('webpack');
-const HtmlWebpackPlugin = require('html-webpack-plugin');
-const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
-const PnpWebpackPlugin = require('pnp-webpack-plugin');
-
-const { commonRules, htmlCommonConfig } = require('./webpack.common');
-
-const config = {
- entry: ['react-hot-loader/patch', './src/app/index.tsx'],
- mode: 'development',
- devtool: 'eval-source-map',
- module: {
- rules: [
- ...commonRules,
- {
- test: /\.(js|jsx|ts|tsx)$/,
- exclude: /node_modules/,
- loader: 'babel-loader',
- options: {
- plugins: ['react-hot-loader/babel'],
- },
- },
- ],
- },
- resolve: {
- alias: {
- 'react-dom': '@hot-loader/react-dom',
- },
- extensions: ['*', '.js', '.jsx', '.ts', '.tsx'],
- plugins: [PnpWebpackPlugin],
- },
- resolveLoader: {
- plugins: [PnpWebpackPlugin.moduleLoader(module)],
- },
- optimization: {
- runtimeChunk: 'single',
- splitChunks: {
- chunks: 'all',
- cacheGroups: {
- vendor: {
- test: /[\\/]node_modules[\\/]/,
- name: 'vendors',
- chunks: 'all',
- },
- },
- },
- },
- output: {
- path: path.resolve(__dirname, 'dist/'),
- filename: '[name].[hash].js',
- chunkFilename: '[name].[hash].js',
- publicPath: '/',
- },
- devServer: {
- contentBase: path.resolve(__dirname, 'public/'),
- host: '0.0.0.0',
- port: 3000,
- publicPath: 'http://localhost:3000/',
- historyApiFallback: true,
- hotOnly: true,
- },
- plugins: [
- new HtmlWebpackPlugin({
- ...htmlCommonConfig,
- devServer: 'http://localhost:3000',
- }),
- new ForkTsCheckerWebpackPlugin({
- tsconfig: './src/app/tsconfig.json',
- }),
- new ForkTsCheckerWebpackPlugin({
- tsconfig: './src/sw/tsconfig.json',
- }),
- new webpack.HotModuleReplacementPlugin(),
- ],
-};
-
-module.exports = config;
diff --git a/Timeline/ClientApp/webpack.config.prod.js b/Timeline/ClientApp/webpack.config.prod.js
deleted file mode 100644
index e4594421..00000000
--- a/Timeline/ClientApp/webpack.config.prod.js
+++ /dev/null
@@ -1,75 +0,0 @@
-const path = require('path');
-const { CleanWebpackPlugin } = require('clean-webpack-plugin');
-const HtmlWebpackPlugin = require('html-webpack-plugin');
-const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
-const CopyPlugin = require('copy-webpack-plugin');
-const PnpWebpackPlugin = require('pnp-webpack-plugin');
-const WorkboxPlugin = require('workbox-webpack-plugin');
-
-const { commonRules, htmlCommonConfig } = require('./webpack.common');
-
-const config = {
- entry: ['./src/app/index.tsx', './src/app/service-worker.tsx'],
- mode: 'production',
- devtool: 'source-map',
- module: {
- rules: [
- ...commonRules,
- {
- test: /\.(js|jsx|ts|tsx)$/,
- exclude: /node_modules/,
- loader: 'babel-loader',
- },
- ],
- },
- resolve: {
- extensions: ['*', '.js', '.jsx', '.ts', '.tsx'],
- plugins: [PnpWebpackPlugin],
- },
- resolveLoader: {
- plugins: [PnpWebpackPlugin.moduleLoader(module)],
- },
- optimization: {
- runtimeChunk: 'single',
- splitChunks: {
- chunks: 'all',
- cacheGroups: {
- vendor: {
- test: /[\\/]node_modules[\\/]/,
- name: 'vendors',
- chunks: 'all',
- },
- },
- },
- },
- output: {
- path: path.resolve(__dirname, 'dist/'),
- filename: '[name].[hash].js',
- chunkFilename: '[name].[hash].js',
- publicPath: '/',
- },
- plugins: [
- new CleanWebpackPlugin(),
- new HtmlWebpackPlugin(htmlCommonConfig),
- new ForkTsCheckerWebpackPlugin({
- tsconfig: './src/app/tsconfig.json',
- }),
- new ForkTsCheckerWebpackPlugin({
- tsconfig: './src/sw/tsconfig.json',
- }),
- new CopyPlugin({
- patterns: [
- {
- from: path.resolve(__dirname, 'public/'),
- to: path.resolve(__dirname, 'dist/'),
- },
- ],
- }),
- new WorkboxPlugin.InjectManifest({
- swSrc: './src/sw/sw.ts',
- maximumFileSizeToCacheInBytes: 15000000,
- }),
- ],
-};
-
-module.exports = config;