4 Commits
test ... master

Author SHA1 Message Date
61391ff2a5 backend support record playback history 2023-01-26 21:21:15 +08:00
3b90ad56d6 Update github CI node version to 18 2022-12-04 20:21:03 +08:00
ec0dad00ef Replace react with preact
reduce js file bundle to 20%
2022-12-04 20:17:59 +08:00
c09e230972 Replace webpack with only esbuild
reduce node_modules size to only 18M
2022-12-04 20:17:56 +08:00
42 changed files with 738 additions and 37727 deletions

View File

@@ -57,7 +57,7 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
node-version: 16 node-version: 18
- name: Build web front end - name: Build web front end
run: | run: |
make web make web

View File

@@ -71,7 +71,7 @@ func NewAPI(config commonconfig.Config) (*API, error) {
// user // user
apiMux.HandleFunc("/login", api.HandleLogin) apiMux.HandleFunc("/login", api.HandleLogin)
apiMux.HandleFunc("/register", api.HandleRegister) apiMux.HandleFunc("/register", api.HandleRegister)
apiMux.HandleFunc("/logout", api.LoginAsAnonymous) apiMux.HandleFunc("/logout", api.HandleLoginAsAnonymous)
apiMux.HandleFunc("/get_user_info", api.HandleGetUserInfo) apiMux.HandleFunc("/get_user_info", api.HandleGetUserInfo)
apiMux.HandleFunc("/get_users", api.HandleGetUsers) apiMux.HandleFunc("/get_users", api.HandleGetUsers)
apiMux.HandleFunc("/update_user_active", api.HandleUpdateUserActive) apiMux.HandleFunc("/update_user_active", api.HandleUpdateUserActive)
@@ -95,6 +95,8 @@ func NewAPI(config commonconfig.Config) (*API, error) {
apiMux.HandleFunc("/update_review", api.HandleUpdateReview) apiMux.HandleFunc("/update_review", api.HandleUpdateReview)
apiMux.HandleFunc("/delete_review", api.HandleDeleteReview) apiMux.HandleFunc("/delete_review", api.HandleDeleteReview)
apiMux.HandleFunc("/get_reviews_by_user", api.HandleGetReviewsByUser) apiMux.HandleFunc("/get_reviews_by_user", api.HandleGetReviewsByUser)
// statistic
apiMux.HandleFunc("/record_playback", api.HandleRecordPlayback)
// database // database
apiMux.HandleFunc("/walk", api.HandleWalk) apiMux.HandleFunc("/walk", api.HandleWalk)
apiMux.HandleFunc("/reset", api.HandleReset) apiMux.HandleFunc("/reset", api.HandleReset)

44
pkg/api/handle_stat.go Normal file
View File

@@ -0,0 +1,44 @@
package api
import (
"encoding/json"
"msw-open-music/pkg/database"
"net/http"
"time"
)
type RecordPlaybackRequest struct {
Playback database.Playback `json:"playback"`
}
func (api *API) HandleRecordPlayback(w http.ResponseWriter, r *http.Request) {
recordPlaybackRequest := &RecordPlaybackRequest{}
err := json.NewDecoder(r.Body).Decode(recordPlaybackRequest)
if err != nil {
api.HandleError(w, r, err)
return
}
recordPlaybackRequest.Playback.Time = time.Now()
recordPlaybackRequest.Playback.UserID, err = api.GetUserID(w, r)
if err != nil {
if err == ErrNotLoggedIn {
user, err := api.Db.LoginAsAnonymous()
if err != nil {
api.HandleError(w, r, err)
return
}
recordPlaybackRequest.Playback.UserID = user.ID
} else {
api.HandleError(w, r, err)
return
}
}
err = api.Db.RecordPlayback(recordPlaybackRequest.Playback)
if err != nil {
api.HandleError(w, r, err)
return
}
api.HandleOK(w, r)
}

View File

@@ -17,23 +17,8 @@ type LoginResponse struct {
User *database.User `json:"user"` User *database.User `json:"user"`
} }
func (api *API) LoginAsAnonymous(w http.ResponseWriter, r *http.Request) { func (api *API) HandleLoginAsAnonymous(w http.ResponseWriter, r *http.Request) {
user, err := api.Db.LoginAsAnonymous() user, err := api.LoginAsAnonymous(w, r)
if err != nil {
api.HandleError(w, r, err)
return
}
session, _ := api.store.Get(r, api.defaultSessionName)
// save session
session.Values["userId"] = user.ID
err = session.Save(r, w)
if err != nil {
api.HandleError(w, r, err)
return
}
resp := &LoginResponse{ resp := &LoginResponse{
User: user, User: user,
} }
@@ -45,6 +30,25 @@ func (api *API) LoginAsAnonymous(w http.ResponseWriter, r *http.Request) {
} }
} }
func (api *API) LoginAsAnonymous(w http.ResponseWriter, r *http.Request) (*database.User, error) {
user, err := api.Db.LoginAsAnonymous()
if err != nil {
return nil, err
}
session, _ := api.store.Get(r, api.defaultSessionName)
// save session
session.Values["userId"] = user.ID
err = session.Save(r, w)
if err != nil {
return nil, err
}
// return user
return user, nil
}
func (api *API) HandleLogin(w http.ResponseWriter, r *http.Request) { func (api *API) HandleLogin(w http.ResponseWriter, r *http.Request) {
var user *database.User var user *database.User
var err error var err error

View File

@@ -0,0 +1,7 @@
package database
func (database *Database) RecordPlayback(playback Playback) error {
_, err := database.stmt.recordPlaybackStmt.Exec(
playback.UserID, playback.FileID, playback.Time, playback.Method, playback.Duration)
return err
}

View File

@@ -2,6 +2,7 @@ package database
import ( import (
"database/sql" "database/sql"
"log"
) )
var initFilesTableQuery = `CREATE TABLE IF NOT EXISTS files ( var initFilesTableQuery = `CREATE TABLE IF NOT EXISTS files (
@@ -87,7 +88,8 @@ var initPlaybacksTableQuery = `CREATE TABLE IF NOT EXISTS playbacks (
user_id INTEGER NOT NULL, user_id INTEGER NOT NULL,
file_id INTEGER NOT NULL, file_id INTEGER NOT NULL,
time INTEGER NOT NULL, time INTEGER NOT NULL,
mothod INTEGER NOT NULL, method INTEGER NOT NULL,
duration INTEGER NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (file_id) REFERENCES files(id) FOREIGN KEY (file_id) REFERENCES files(id)
);` );`
@@ -285,6 +287,8 @@ var updateFilenameQuery = `UPDATE files SET filename = ? WHERE id = ?;`
var resetFilenameQuery = `UPDATE files SET filename = realname WHERE id = ?;` var resetFilenameQuery = `UPDATE files SET filename = realname WHERE id = ?;`
var recordPlaybackQuery = `INSERT INTO playbacks (user_id, file_id, time, method, duration) VALUES ($1, $2, $3, $4, $5);`
type Stmt struct { type Stmt struct {
initFilesTable *sql.Stmt initFilesTable *sql.Stmt
initFoldersTable *sql.Stmt initFoldersTable *sql.Stmt
@@ -345,6 +349,7 @@ type Stmt struct {
deleteFileReferenceInReviews *sql.Stmt deleteFileReferenceInReviews *sql.Stmt
updateFilename *sql.Stmt updateFilename *sql.Stmt
resetFilename *sql.Stmt resetFilename *sql.Stmt
recordPlaybackStmt *sql.Stmt
} }
func NewPreparedStatement(sqlConn *sql.DB) (*Stmt, error) { func NewPreparedStatement(sqlConn *sql.DB) (*Stmt, error) {
@@ -772,5 +777,12 @@ func NewPreparedStatement(sqlConn *sql.DB) (*Stmt, error) {
return nil, err return nil, err
} }
stmt.recordPlaybackStmt, err = sqlConn.Prepare(recordPlaybackQuery)
if err != nil {
return nil, err
}
log.Println("Init statements finished")
return stmt, err return stmt, err
} }

View File

@@ -2,6 +2,7 @@ package database
import ( import (
"path/filepath" "path/filepath"
"time"
) )
type File struct { type File struct {
@@ -58,6 +59,15 @@ type Feedback struct {
Time int64 `json:"time"` Time int64 `json:"time"`
} }
type Playback struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
FileID int64 `json:"file_id"`
Time time.Time `json:"time"`
Method int64 `json:"method"`
Duration time.Duration `json:"Duration"`
}
var ( var (
RoleAnonymous = int64(0) RoleAnonymous = int64(0)
RoleAdmin = int64(1) RoleAdmin = int64(1)

View File

@@ -1,70 +1,9 @@
# Getting Started with Create React App # MSW Open Music Web Frontend
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). This is a React single page application. And use Preact instead of React to achieve a smaller file size.
## Available Scripts `node_modules` only has 19M. We uses esbuild and shell scripts and build only takes a milliseconds!
In the project directory, you can run: ## How to build
### `npm start` Simple run `./build.sh`, then all output files are under `./build/` directory.
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.\
You will also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
### Analyzing the Bundle Size
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
### Making a Progressive Web App
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
### Advanced Configuration
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
### Deployment
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
### `npm run build` fails to minify
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)

6
web/build.sh Executable file
View File

@@ -0,0 +1,6 @@
rm -rf build
cp -raf public build
./node_modules/.bin/esbuild src/index.jsx --bundle --outfile=build/msw-open-music.js --alias:react=preact/compat --alias:react-dom=preact/compat --minify --analyze
cat public/index.html | sed "s/%PUBLIC_URL%/$PUBLIC_URL/" > build/index.html
echo "Build done, output files under ./build directory"

38125
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,42 +3,12 @@
"version": "1.2.0", "version": "1.2.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@testing-library/jest-dom": "^5.15.0", "@preact/compat": "^17.1.2",
"@testing-library/react": "^11.2.7", "esbuild": "^0.15.17",
"@testing-library/user-event": "^12.8.3", "react-router-dom": "^6.4.4",
"react": "^17.0.2", "water.css": "^2.1.1"
"react-dom": "^17.0.2",
"react-router": "^6.3.0",
"react-router-dom": "^6.3.0",
"react-scripts": "^4.0.3",
"water.css": "^2.1.1",
"web-vitals": "^1.1.2"
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "build": "bash ./build.sh"
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@types/react": "^17.0.34"
} }
} }

View File

@@ -6,36 +6,14 @@
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="Personal music streaming platform" /> <meta name="description" content="Personal music streaming platform" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/favicon.png" /> <link rel="apple-touch-icon" href="%PUBLIC_URL%/favicon.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!-- <link rel="stylesheet" href="%PUBLIC_URL%/msw-open-music.css" />
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<!-- Add to homescreen for Chrome on Android -->
<meta name="mobile-web-app-capable" content="yes" /> <meta name="mobile-web-app-capable" content="yes" />
<title>MSW Open Music</title> <title>MSW Open Music</title>
</head> </head>
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div> <div id="root"></div>
<!-- <script type="module" src="%PUBLIC_URL%/msw-open-music.js"></script>
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body> </body>
</html> </html>

View File

@@ -1,3 +1,4 @@
import * as React from 'react';
import { HashRouter as Router, Routes, Route, NavLink } from "react-router-dom"; import { HashRouter as Router, Routes, Route, NavLink } from "react-router-dom";
import "./App.css"; import "./App.css";

View File

@@ -1,6 +1,7 @@
import { useEffect, useState } from "react"; import * as React from 'react';
import { useNavigate } from "react-router"; import {useEffect, useState} from "react";
import { CalcReadableFilesizeDetail } from "./Common"; import {useNavigate} from "react-router";
import {CalcReadableFilesizeDetail} from "./Common";
import FfmpegConfig from "./FfmpegConfig"; import FfmpegConfig from "./FfmpegConfig";
import FileDialog from "./FileDialog"; import FileDialog from "./FileDialog";
import { Tr } from "../translate"; import { Tr } from "../translate";

View File

@@ -1,3 +1,4 @@
import * as React from 'react';
import { useState, useEffect, useContext } from "react"; import { useState, useEffect, useContext } from "react";
import { Tr, tr, langCodeContext } from "../translate"; import { Tr, tr, langCodeContext } from "../translate";

View File

@@ -1,3 +1,4 @@
import * as React from 'react';
import { useContext, useEffect, useState } from "react"; import { useContext, useEffect, useState } from "react";
import { useParams, useNavigate } from "react-router"; import { useParams, useNavigate } from "react-router";
import { tr, Tr, langCodeContext } from "../translate"; import { tr, Tr, langCodeContext } from "../translate";

View File

@@ -1,3 +1,4 @@
import * as React from 'react';
import { useState, useEffect, useContext } from "react"; import { useState, useEffect, useContext } from "react";
import { useParams, useNavigate } from "react-router"; import { useParams, useNavigate } from "react-router";
import { tr, Tr, langCodeContext } from "../translate"; import { tr, Tr, langCodeContext } from "../translate";

View File

@@ -1,3 +1,4 @@
import * as React from 'react';
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { convertIntToDateTime } from "./Common"; import { convertIntToDateTime } from "./Common";

View File

@@ -1,3 +1,4 @@
import * as React from 'react';
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
function FfmpegConfig(props) { function FfmpegConfig(props) {

View File

@@ -1,3 +1,4 @@
import * as React from 'react';
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import { Tr } from "../translate"; import { Tr } from "../translate";

View File

@@ -1,3 +1,4 @@
import * as React from 'react';
import { useState } from "react"; import { useState } from "react";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import { CalcReadableFilesize } from "./Common"; import { CalcReadableFilesize } from "./Common";

View File

@@ -1,3 +1,4 @@
import * as React from 'react';
import {useNavigate, useParams} from "react-router"; import {useNavigate, useParams} from "react-router";
import {useContext, useEffect, useState} from "react"; import {useContext, useEffect, useState} from "react";
import {Tr, tr, langCodeContext} from "../translate"; import {Tr, tr, langCodeContext} from "../translate";

View File

@@ -1,3 +1,4 @@
import * as React from 'react';
import {useParams} from "react-router"; import {useParams} from "react-router";
import {useState, useEffect} from "react"; import {useState, useEffect} from "react";
import {useNavigate} from "react-router-dom"; import {useNavigate} from "react-router-dom";

View File

@@ -1,3 +1,4 @@
import * as React from 'react';
import FileEntry from "./FileEntry"; import FileEntry from "./FileEntry";
import { Tr } from "../translate"; import { Tr } from "../translate";

View File

@@ -1,3 +1,4 @@
import * as React from 'react';
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import { Tr } from "../translate"; import { Tr } from "../translate";

View File

@@ -1,3 +1,4 @@
import * as React from 'react';
import { useContext, useEffect, useState } from "react"; import { useContext, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useQuery } from "./Common"; import { useQuery } from "./Common";

View File

@@ -1,3 +1,4 @@
import * as React from 'react';
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useContext, useState } from "react"; import { useContext, useState } from "react";
import { Tr, tr, langCodeContext } from "../translate"; import { Tr, tr, langCodeContext } from "../translate";

View File

@@ -1,3 +1,4 @@
import * as React from 'react';
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import Database from "./Database"; import Database from "./Database";

View File

@@ -1,3 +1,4 @@
import * as React from 'react';
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Tr } from "../translate"; import { Tr } from "../translate";

View File

@@ -1,3 +1,4 @@
import * as React from 'react';
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useContext, useState } from "react"; import { useContext, useState } from "react";
import { tr, Tr, langCodeContext } from "../translate"; import { tr, Tr, langCodeContext } from "../translate";

View File

@@ -1,3 +1,4 @@
import * as React from 'react';
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { convertIntToDateTime } from "./Common"; import { convertIntToDateTime } from "./Common";
import { Tr, tr, langCodeContext } from "../translate"; import { Tr, tr, langCodeContext } from "../translate";

View File

@@ -1,3 +1,4 @@
import * as React from 'react';
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useParams } from "react-router"; import { useParams } from "react-router";
import ReviewEntry from "./ReviewEntry"; import ReviewEntry from "./ReviewEntry";

View File

@@ -1,3 +1,4 @@
import * as React from 'react';
import { useState, useEffect, useContext } from "react"; import { useState, useEffect, useContext } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useQuery } from "./Common"; import { useQuery } from "./Common";

View File

@@ -1,3 +1,4 @@
import * as React from 'react';
import { useContext, useEffect, useState } from "react"; import { useContext, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useQuery } from "./Common"; import { useQuery } from "./Common";

View File

@@ -1,3 +1,4 @@
import * as React from 'react';
import { useContext, useEffect, useState } from "react"; import { useContext, useEffect, useState } from "react";
import { useParams } from "react-router"; import { useParams } from "react-router";
import FilesTable from "./FilesTable"; import FilesTable from "./FilesTable";

View File

@@ -1,3 +1,4 @@
import * as React from 'react';
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Tr } from "../translate"; import { Tr } from "../translate";

View File

@@ -1,3 +1,4 @@
import * as React from 'react';
import { useState, useEffect, useContext } from "react"; import { useState, useEffect, useContext } from "react";
import { useParams } from "react-router"; import { useParams } from "react-router";
import ReviewEntry from "./ReviewEntry"; import ReviewEntry from "./ReviewEntry";

View File

@@ -1,3 +1,4 @@
import * as React from 'react';
import { useEffect } from 'react'; import { useEffect } from 'react';
function UserStatus(props) { function UserStatus(props) {

View File

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

12
web/src/index.jsx Normal file
View File

@@ -0,0 +1,12 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import 'water.css';
import App from './App';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);

View File

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

View File

@@ -1,4 +1,5 @@
import { createContext, renderToString } from "react"; import * as React from 'react';
import { createContext } from "react";
import MAP_zh_CN from "./zh_CN"; import MAP_zh_CN from "./zh_CN";
const LANG_OPTIONS = { const LANG_OPTIONS = {