Procházet zdrojové kódy

feat: add dict memo page

RegMs If před 3 roky
rodič
revize
eaf40fad4e

+ 21 - 0
.eslintrc.cjs

@@ -0,0 +1,21 @@
+module.exports = {
+  extends: [
+    'eslint:recommended',
+    'plugin:@typescript-eslint/recommended',
+    'plugin:@typescript-eslint/recommended-requiring-type-checking',
+    'plugin:@typescript-eslint/strict',
+    'plugin:react/recommended',
+    'plugin:react-hooks/recommended',
+  ],
+  parser: '@typescript-eslint/parser',
+  parserOptions: {
+    project: ['./tsconfig.json', './tsconfig.node.json'],
+  },
+  plugins: ['@typescript-eslint', 'react', 'react-hooks'],
+  root: true,
+  settings: {
+    react: {
+      version: 'detect',
+    },
+  },
+};

+ 29 - 0
.gitignore

@@ -0,0 +1,29 @@
+# ---> Node
+# Logs
+logs
+*.log
+npm-debug.log*
+
+# Runtime data
+pids
+*.pid
+*.seed
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+
+# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (http://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directory
+# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
+node_modules

+ 9 - 0
.prettierrc

@@ -0,0 +1,9 @@
+{
+  "arrowParens": "avoid",
+  "bracketSpacing": true,
+  "semi": true,
+  "singleQuote": true,
+  "tabWidth": 2,
+  "trailingComma": "all",
+  "useTabs": false
+}

+ 1 - 0
README.md

@@ -0,0 +1 @@
+# Project Woord (Front-end)

+ 1 - 1
index.html

@@ -4,7 +4,7 @@
     <meta charset="UTF-8" />
     <link rel="icon" type="image/svg+xml" href="/vite.svg" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-    <title>Vite + React + TS</title>
+    <title>Woord</title>
   </head>
   <body>
     <div id="root"></div>

+ 10 - 2
package.json

@@ -10,14 +10,22 @@
   },
   "dependencies": {
     "antd": "^4.23.4",
+    "lodash": "^4.17.21",
     "react": "^18.2.0",
-    "react-dom": "^18.2.0"
+    "react-dom": "^18.2.0",
+    "react-router-dom": "^6.4.1"
   },
   "devDependencies": {
+    "@types/lodash": "^4.14.186",
     "@types/react": "^18.0.17",
     "@types/react-dom": "^18.0.6",
+    "@typescript-eslint/eslint-plugin": "^5.39.0",
+    "@typescript-eslint/parser": "^5.39.0",
     "@vitejs/plugin-react": "^2.1.0",
-    "typescript": "^4.6.4",
+    "eslint": "^8.24.0",
+    "eslint-plugin-react": "^7.31.8",
+    "eslint-plugin-react-hooks": "^4.6.0",
+    "typescript": "^4.8.4",
     "vite": "^3.1.0"
   }
 }

+ 0 - 41
src/App.css

@@ -1,41 +0,0 @@
-#root {
-  max-width: 1280px;
-  margin: 0 auto;
-  padding: 2rem;
-  text-align: center;
-}
-
-.logo {
-  height: 6em;
-  padding: 1.5em;
-  will-change: filter;
-}
-.logo:hover {
-  filter: drop-shadow(0 0 2em #646cffaa);
-}
-.logo.react:hover {
-  filter: drop-shadow(0 0 2em #61dafbaa);
-}
-
-@keyframes logo-spin {
-  from {
-    transform: rotate(0deg);
-  }
-  to {
-    transform: rotate(360deg);
-  }
-}
-
-@media (prefers-reduced-motion: no-preference) {
-  a:nth-of-type(2) .logo {
-    animation: logo-spin infinite 20s linear;
-  }
-}
-
-.card {
-  padding: 2em;
-}
-
-.read-the-docs {
-  color: #888;
-}

+ 5 - 32
src/App.tsx

@@ -1,34 +1,7 @@
-import { useState } from 'react'
-import reactLogo from './assets/react.svg'
-import './App.css'
+import React from 'react';
+import { RouterProvider } from 'react-router-dom';
+import router from './router';
 
-function App() {
-  const [count, setCount] = useState(0)
+const App: React.FC = () => <RouterProvider router={router} />;
 
-  return (
-    <div className="App">
-      <div>
-        <a href="https://vitejs.dev" target="_blank">
-          <img src="/vite.svg" className="logo" alt="Vite logo" />
-        </a>
-        <a href="https://reactjs.org" target="_blank">
-          <img src={reactLogo} className="logo react" alt="React logo" />
-        </a>
-      </div>
-      <h1>Vite + React</h1>
-      <div className="card">
-        <button onClick={() => setCount((count) => count + 1)}>
-          count is {count}
-        </button>
-        <p>
-          Edit <code>src/App.tsx</code> and save to test HMR
-        </p>
-      </div>
-      <p className="read-the-docs">
-        Click on the Vite and React logos to learn more
-      </p>
-    </div>
-  )
-}
-
-export default App
+export default App;

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
src/assets/react.svg


+ 0 - 50
src/index.css

@@ -4,10 +4,6 @@
   line-height: 24px;
   font-weight: 400;
 
-  color-scheme: light dark;
-  color: rgba(255, 255, 255, 0.87);
-  background-color: #242424;
-
   font-synthesis: none;
   text-rendering: optimizeLegibility;
   -webkit-font-smoothing: antialiased;
@@ -15,15 +11,6 @@
   -webkit-text-size-adjust: 100%;
 }
 
-a {
-  font-weight: 500;
-  color: #646cff;
-  text-decoration: inherit;
-}
-a:hover {
-  color: #535bf2;
-}
-
 body {
   margin: 0;
   display: flex;
@@ -31,40 +18,3 @@ body {
   min-width: 320px;
   min-height: 100vh;
 }
-
-h1 {
-  font-size: 3.2em;
-  line-height: 1.1;
-}
-
-button {
-  border-radius: 8px;
-  border: 1px solid transparent;
-  padding: 0.6em 1.2em;
-  font-size: 1em;
-  font-weight: 500;
-  font-family: inherit;
-  background-color: #1a1a1a;
-  cursor: pointer;
-  transition: border-color 0.25s;
-}
-button:hover {
-  border-color: #646cff;
-}
-button:focus,
-button:focus-visible {
-  outline: 4px auto -webkit-focus-ring-color;
-}
-
-@media (prefers-color-scheme: light) {
-  :root {
-    color: #213547;
-    background-color: #ffffff;
-  }
-  a:hover {
-    color: #747bff;
-  }
-  button {
-    background-color: #f9f9f9;
-  }
-}

+ 18 - 0
src/layout/PageLayout.module.css

@@ -0,0 +1,18 @@
+.layout {
+  width: 100vw;
+  height: 100vh;
+}
+
+.header {
+  background-color: white;
+  box-shadow: 0px 4px 4px -2px lightgray;
+  z-index: 1;
+}
+
+.title {
+  font-size: 24px;
+}
+
+.content {
+  background-color: white;
+}

+ 22 - 0
src/layout/PageLayout.tsx

@@ -0,0 +1,22 @@
+import React from 'react';
+import { Outlet } from 'react-router-dom';
+import { Layout, Typography } from 'antd';
+import styles from './PageLayout.module.css';
+
+const { Header, Content } = Layout;
+const { Text } = Typography;
+
+const PageLayout: React.FC = () => (
+  <Layout className={styles.layout}>
+    <Header className={styles.header}>
+      <Text className={styles.title} strong>
+        Woord
+      </Text>
+    </Header>
+    <Content className={styles.content}>
+      <Outlet />
+    </Content>
+  </Layout>
+);
+
+export default PageLayout;

+ 7 - 6
src/main.tsx

@@ -1,10 +1,11 @@
-import React from 'react'
-import ReactDOM from 'react-dom/client'
-import App from './App'
-import './index.css'
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import './index.css';
+import 'antd/dist/antd.css';
+import App from './App';
 
 ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
   <React.StrictMode>
     <App />
-  </React.StrictMode>
-)
+  </React.StrictMode>,
+);

+ 18 - 0
src/pages/MemoPage.module.css

@@ -0,0 +1,18 @@
+.wrapper {
+  height: 100%;
+  display: flex;
+}
+
+.space {
+  width: 350px;
+  margin: auto;
+}
+
+.primaryText {
+  font-size: 48px;
+}
+
+.secondaryText {
+  line-height: 32px;
+  text-align: center;
+}

+ 173 - 0
src/pages/MemoPage.tsx

@@ -0,0 +1,173 @@
+import React, { useState, useEffect } from 'react';
+import _ from 'lodash';
+import { Space, Row, Col, Button, Typography, Empty } from 'antd';
+import {
+  EyeOutlined,
+  StarFilled,
+  StarOutlined,
+  ArrowLeftOutlined,
+  PlusOutlined,
+  ReloadOutlined,
+  ArrowRightOutlined,
+} from '@ant-design/icons';
+import type { Dict } from '../types/dicts';
+import styles from './MemoPage.module.css';
+
+const { Text } = Typography;
+
+const dict: Dict = {
+  valueTitle: '日文',
+  meaningTitle: '中文',
+  extraTitle: '假名',
+  words: [
+    { value: 'test', meaning: '测试', extra: '附加?', star: true },
+    { value: 'good', meaning: '好', extra: 'no!', star: false },
+  ],
+};
+
+enum Mode {
+  ValueToMeaning,
+  MeaningToValue,
+}
+
+const MemoPage: React.FC = () => {
+  const [mode, setMode] = useState<Mode>(Mode.ValueToMeaning);
+  const [words, setWords] = useState<Dict['words']>([]);
+  const [index, setIndex] = useState(0);
+  const [showValueOrMeaning, setShowValueOrMeaning] = useState(false);
+  const [showExtra, setShowExtra] = useState(false);
+
+  useEffect(() => {
+    setWords(_.shuffle(dict.words));
+  }, []);
+
+  const updateIndex = (index: number) => {
+    setIndex(index);
+    setShowValueOrMeaning(false);
+    setShowExtra(false);
+  };
+
+  return (
+    <div className={styles.wrapper}>
+      <Space className={styles.space} size="large" direction="vertical">
+        <Row justify="space-around">
+          <Col>
+            <Button
+              type={mode === Mode.ValueToMeaning ? 'primary' : 'default'}
+              onClick={() => setMode(Mode.ValueToMeaning)}
+            >
+              {dict.valueTitle} → {dict.meaningTitle}
+            </Button>
+          </Col>
+          <Col>
+            <Button
+              type={mode === Mode.MeaningToValue ? 'primary' : 'default'}
+              onClick={() => setMode(Mode.MeaningToValue)}
+            >
+              {dict.meaningTitle} → {dict.valueTitle}
+            </Button>
+          </Col>
+        </Row>
+        {words.length ? (
+          <>
+            <Row justify="center">
+              <Col className={styles.primaryText}>
+                <Text strong>
+                  {mode === Mode.ValueToMeaning
+                    ? words[index].value
+                    : words[index].meaning}
+                </Text>
+              </Col>
+            </Row>
+            <Row>
+              <Col className={styles.secondaryText} flex="1">
+                {showValueOrMeaning ? (
+                  <Text>
+                    {mode === Mode.MeaningToValue
+                      ? words[index].value
+                      : words[index].meaning}
+                  </Text>
+                ) : (
+                  <Button
+                    type="text"
+                    block
+                    icon={<EyeOutlined />}
+                    onClick={() => setShowValueOrMeaning(true)}
+                  >
+                    {mode === Mode.MeaningToValue
+                      ? dict.valueTitle
+                      : dict.meaningTitle}
+                  </Button>
+                )}
+              </Col>
+              <Col className={styles.secondaryText} flex="1">
+                {showExtra ? (
+                  <Text>{words[index].extra}</Text>
+                ) : (
+                  <Button
+                    type="text"
+                    block
+                    icon={<EyeOutlined />}
+                    onClick={() => setShowExtra(true)}
+                  >
+                    {dict.extraTitle}
+                  </Button>
+                )}
+              </Col>
+            </Row>
+            <Row justify="center">
+              {words[index].star ? (
+                <Button type="primary" danger icon={<StarFilled />}>
+                  取消易错词
+                </Button>
+              ) : (
+                <Button danger icon={<StarOutlined />}>
+                  标记易错词
+                </Button>
+              )}
+            </Row>
+          </>
+        ) : (
+          <Empty />
+        )}
+        <Row>
+          <Col flex="1">
+            <Button
+              type="link"
+              block
+              disabled={index <= 0}
+              onClick={() => updateIndex(index - 1)}
+            >
+              <ArrowLeftOutlined /> 上一个
+            </Button>
+          </Col>
+          <Col>
+            <Button type="dashed" icon={<PlusOutlined />}>
+              输入新单词
+            </Button>
+          </Col>
+          <Col flex="1">
+            {index >= words.length - 1 ? (
+              <Button
+                type="link"
+                block
+                onClick={() => {
+                  setWords(_.shuffle(dict.words));
+                  updateIndex(0);
+                }}
+              >
+                再来一次 <ReloadOutlined />
+              </Button>
+            ) : (
+              <Button type="link" block onClick={() => updateIndex(index + 1)}>
+                下一个 <ArrowRightOutlined />
+              </Button>
+            )}
+          </Col>
+        </Row>
+      </Space>
+    </div>
+  );
+};
+
+export default MemoPage;

+ 26 - 0
src/router/index.tsx

@@ -0,0 +1,26 @@
+import React from 'react';
+import { createBrowserRouter } from 'react-router-dom';
+import PageLayout from '../layout/PageLayout';
+import MemoPage from '../pages/MemoPage';
+
+export default createBrowserRouter([
+  {
+    path: '/',
+    element: <PageLayout />,
+    children: [
+      {
+        path: 'dicts',
+        children: [
+          { index: true, element: '233' },
+          {
+            path: ':dictId',
+            children: [
+              { index: true, element: '666' },
+              { path: 'memo', element: <MemoPage /> },
+            ],
+          },
+        ],
+      },
+    ],
+  },
+]);

+ 9 - 0
src/types/dicts.ts

@@ -0,0 +1,9 @@
+export interface Word {
+  value: string;
+  meaning: string;
+  extra: string;
+}
+
+export type Dict = {
+  [field in keyof Word as `${field}Title`]: string;
+} & { words: (Word & { star: boolean })[] };

+ 4 - 4
vite.config.ts

@@ -1,7 +1,7 @@
-import { defineConfig } from 'vite'
-import react from '@vitejs/plugin-react'
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
 
 // https://vitejs.dev/config/
 export default defineConfig({
-  plugins: [react()]
-})
+  plugins: [react()],
+});

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 855 - 1
yarn.lock


Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů