refactor: rebuild project struction

pull/13/head
moonrailgun 4 years ago
parent bd441a45eb
commit 26fd3c6cdb

@ -0,0 +1,7 @@
import { EffectCallback, useEffect } from 'react';
// Reference: https://github.com/streamich/react-use/blob/master/src/useEffectOnce.ts
export const useEffectOnce = (effect: EffectCallback) => {
useEffect(effect, []);
};

@ -0,0 +1,26 @@
import { Dispatch, SetStateAction, useCallback, useRef, useState } from 'react';
import { useUnmount } from './useUnmount';
// Reference: https://github.com/streamich/react-use/blob/master/src/useRafState.ts
export const useRafState = <S>(
initialState: S | (() => S)
): [S, Dispatch<SetStateAction<S>>] => {
const frame = useRef(0);
const [state, setState] = useState(initialState);
const setRafState = useCallback((value: S | ((prevState: S) => S)) => {
cancelAnimationFrame(frame.current);
frame.current = requestAnimationFrame(() => {
setState(value);
});
}, []);
useUnmount(() => {
cancelAnimationFrame(frame.current);
});
return [state, setRafState];
};

@ -0,0 +1,13 @@
import { useRef } from 'react';
import { useEffectOnce } from './useEffectOnce';
// Reference: https://github.com/streamich/react-use/blob/master/src/useUnmount.ts
export const useUnmount = (fn: () => any): void => {
const fnRef = useRef(fn);
// update the ref each render so if it change the newest callback will be invoked
fnRef.current = fn;
useEffectOnce(() => () => fnRef.current());
};

@ -22,6 +22,7 @@ export { t, setLanguage, useTranslation } from './i18n';
export { useAsync } from './hooks/useAsync';
export { useAsyncFn } from './hooks/useAsyncFn';
export { useMountedState } from './hooks/useMountedState';
export { useRafState } from './hooks/useRafState';
// manager
export { getStorage, setStorage, useStorage } from './manager/storage';
@ -39,3 +40,4 @@ export type { AppStore, AppDispatch } from './redux/store';
// utils
export { getTextColorHex } from './utils/string-helper';
export { isBrowser, isNavigator } from './utils/environment';

@ -0,0 +1,3 @@
export const isBrowser = typeof window !== 'undefined';
export const isNavigator = typeof navigator !== 'undefined';

@ -24,6 +24,7 @@
"react-redux": "^7.2.4",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
"react-use-gesture": "^9.1.3",
"socket.io-client": "^4.1.2",
"tailwindcss": "^2.2.4",
"yup": "^0.32.9"
@ -56,6 +57,7 @@
"style-loader": "^3.0.0",
"ts-node": "^10.0.0",
"tsconfig-paths": "^3.9.0",
"tsconfig-paths-webpack-plugin": "^3.5.1",
"typescript": "^4.3.4",
"url-loader": "^4.1.1",
"webpack": "^5.41.0",

@ -0,0 +1,10 @@
import { useWindowSize } from './useWindowSize';
/**
*
*/
export function useIsMobile(): boolean {
const { width } = useWindowSize();
return width < 768;
}

@ -0,0 +1,33 @@
import { isBrowser, useRafState } from 'pawchat-shared';
import { useEffect } from 'react';
// Reference: https://github.com/streamich/react-use/blob/master/src/useWindowSize.ts
export const useWindowSize = (
initialWidth = Infinity,
initialHeight = Infinity
) => {
const [state, setState] = useRafState<{ width: number; height: number }>({
width: isBrowser ? window.innerWidth : initialWidth,
height: isBrowser ? window.innerHeight : initialHeight,
});
useEffect((): (() => void) | void => {
if (isBrowser) {
const handler = () => {
setState({
width: window.innerWidth,
height: window.innerHeight,
});
};
window.addEventListener('resize', handler);
return () => {
window.removeEventListener('resize', handler);
};
}
}, []);
return state;
};

@ -1,52 +0,0 @@
import React from 'react';
import { PillTabs, PillTabPane } from '../../components/PillTabs';
import { useAppSelector } from '../../hooks/useAppSelector';
/**
*
*/
export const Content: React.FC = React.memo(() => {
const friends = useAppSelector((state) => state.user.friends);
const friendRequests = useAppSelector((state) => state.user.friendRequests);
const userId = useAppSelector((state) => state.user.info?._id);
return (
<div className="flex-auto bg-gray-700">
<PillTabs>
<PillTabPane tab={'全部'} key="1">
<div className="py-2.5 px-5">
<div></div>
<div>{JSON.stringify(friends)}</div>
</div>
</PillTabPane>
<PillTabPane tab={'已发送'} key="2">
<div className="py-2.5 px-5">
<div></div>
<div>
{JSON.stringify(
friendRequests.filter((item) => item.from === userId)
)}
</div>
</div>
</PillTabPane>
<PillTabPane tab={'待处理'} key="3">
<div className="py-2.5 px-5">
<div></div>
<div>
{JSON.stringify(
friendRequests.filter((item) => item.to === userId)
)}
</div>
</div>
</PillTabPane>
<PillTabPane
tab={<span className="text-green-400"></span>}
key="4"
>
</PillTabPane>
</PillTabs>
</div>
);
});
Content.displayName = 'Content';

@ -0,0 +1,132 @@
import React, { useCallback } from 'react';
import { useSidebarContext } from '../SidebarContext';
import _isNil from 'lodash/isNil';
import { useDrag } from 'react-use-gesture';
import { useIsMobile } from '@/hooks/useIsMobile';
import clsx from 'clsx';
// const PageContentRoot = styled.div`
// display: flex;
// flex-direction: row;
// flex: 1;
// overflow: hidden;
// `;
// const ContentDetail = styled.div`
// flex: 1;
// background-color: ${(props) => props.theme.style.contentBackgroundColor};
// display: flex;
// flex-direction: column;
// position: relative;
// overflow: hidden;
// @media (max-width: 768px) {
// width: ${(props) => `calc(100vw - ${props.theme.style.navbarWidth})`};
// min-width: ${(props) => `calc(100vw - ${props.theme.style.navbarWidth})`};
// }
// `;
// const ContentDetailMask = styled.div`
// position: absolute;
// top: 0;
// left: 0;
// right: 0;
// bottom: 0;
// z-index: 10;
// `;
// const SidebarContainer = styled.div<{
// showSidebar: boolean;
// }>`
// ${(props) => props.theme.mixins.transition('width', 0.2)};
// width: ${(props) => (props.showSidebar ? props.theme.style.sidebarWidth : 0)};
// background-color: ${(props) => props.theme.style.sidebarBackgroundColor};
// overflow: hidden;
// display: flex;
// flex-direction: column;
// flex: none;
// `;
const PageContentRoot: React.FC = (props) => (
<div className="flex flex-row flex-1 overflow-hidden">{props.children}</div>
);
interface PageContentProps {
sidebar?: React.ReactNode;
}
const PageGestureWrapper: React.FC = React.memo((props) => {
const { setShowSidebar } = useSidebarContext();
const bind = useDrag(
(state) => {
const { swipe } = state;
const swipeX = swipe[0];
if (swipeX > 0) {
setShowSidebar(true);
} else if (swipeX < 0) {
setShowSidebar(false);
}
},
{
axis: 'x',
swipeDistance: 5,
}
);
return <PageContentRoot {...bind()}>{props.children}</PageContentRoot>;
});
PageGestureWrapper.displayName = 'PageGestureWrapper';
/**
*
*/
export const PageContent: React.FC<PageContentProps> = React.memo((props) => {
const { sidebar, children } = props;
const { showSidebar, setShowSidebar } = useSidebarContext();
const isMobile = useIsMobile();
const handleHideSidebar = useCallback(() => {
setShowSidebar(false);
}, []);
const sidebarEl = _isNil(sidebar) ? null : (
<div
className={clsx('bg-gray-800 p-2', {
'w-60': showSidebar,
})}
>
{props.sidebar}
</div>
);
// 是否显示遮罩层
const showMask =
isMobile === true && showSidebar === true && !_isNil(sidebarEl);
const contentMaskEl = showMask ? (
<div
className="absolute left-0 top-0 bottom-0 right-0 z-10"
onClick={handleHideSidebar}
/>
) : null;
const contentEl = children;
const el = (
<>
{sidebarEl}
<div className="flex flex-auto bg-gray-700 relative">
{contentMaskEl}
{contentEl}
</div>
</>
);
if (isMobile) {
return <PageGestureWrapper>{el}</PageGestureWrapper>;
} else {
return <PageContentRoot>{el}</PageContentRoot>;
}
});
PageContent.displayName = 'PageContent';

@ -0,0 +1,50 @@
import React from 'react';
import { useAppSelector } from '@/hooks/useAppSelector';
import { PillTabPane, PillTabs } from '@/components/PillTabs';
/**
*
*/
export const FriendPanel: React.FC = React.memo(() => {
const friends = useAppSelector((state) => state.user.friends);
const friendRequests = useAppSelector((state) => state.user.friendRequests);
const userId = useAppSelector((state) => state.user.info?._id);
return (
<PillTabs>
<PillTabPane tab={'全部'} key="1">
<div className="py-2.5 px-5">
<div></div>
<div>{JSON.stringify(friends)}</div>
</div>
</PillTabPane>
<PillTabPane tab={'已发送'} key="2">
<div className="py-2.5 px-5">
<div></div>
<div>
{JSON.stringify(
friendRequests.filter((item) => item.from === userId)
)}
</div>
</div>
</PillTabPane>
<PillTabPane tab={'待处理'} key="3">
<div className="py-2.5 px-5">
<div></div>
<div>
{JSON.stringify(
friendRequests.filter((item) => item.to === userId)
)}
</div>
</div>
</PillTabPane>
<PillTabPane
tab={<span className="text-green-400"></span>}
key="4"
>
</PillTabPane>
</PillTabs>
);
});
FriendPanel.displayName = 'FriendPanel';

@ -1,7 +1,7 @@
import React from 'react';
import clsx, { ClassValue } from 'clsx';
import { Icon } from '@iconify/react';
import { Avatar } from '../../components/Avatar';
import { Avatar } from '@/components/Avatar';
const SidebarItem: React.FC<{
className?: ClassValue;
@ -44,12 +44,11 @@ const SidebarSection: React.FC<{
SidebarSection.displayName = 'SidebarSection';
/**
*
*
*/
export const Sidebar: React.FC = React.memo(() => {
return (
<div className="w-60 bg-gray-800 p-2">
{/* Sidebar */}
<>
<SidebarItem icon={<Icon icon="mdi-account-multiple" />}>
</SidebarItem>
@ -81,7 +80,7 @@ export const Sidebar: React.FC = React.memo(() => {
>
1
</SidebarItem>
</div>
</>
);
});
Sidebar.displayName = 'Sidebar';

@ -0,0 +1,18 @@
import React from 'react';
import { Redirect, Route, Switch } from 'react-router-dom';
import { PageContent } from '../PageContent';
import { FriendPanel } from './Friends';
import { Sidebar } from './Sidebar';
export const Personal: React.FC = React.memo(() => {
return (
<PageContent sidebar={<Sidebar />}>
<Switch>
<Route path="/main/personal/friends" component={FriendPanel} />
<Redirect to="/main/personal/friends" />
</Switch>
</PageContent>
);
});
Personal.displayName = 'Personal';

@ -0,0 +1,19 @@
import React from 'react';
import { Personal } from './Personal';
import { Route, Switch, Redirect } from 'react-router-dom';
export const MainContent: React.FC = React.memo(() => {
return (
<Switch>
<Route path="/main/personal" component={Personal} />
{/* <Route path="/main/group/add">
<AddGroup />
</Route>
<Route path="/main/group/:groupUUID">
<Group />
</Route> */}
<Redirect to="/main/personal" />
</Switch>
);
});
MainContent.displayName = 'MainContent';

@ -5,7 +5,7 @@ import {
useAsync,
userActions,
} from 'pawchat-shared';
import React, { useMemo } from 'react';
import React from 'react';
import { LoadingSpinner } from '../../components/LoadingSpinner';
import { Provider as ReduxProvider } from 'react-redux';
import { getGlobalUserLoginInfo } from '../../utils/user-helper';
@ -13,6 +13,7 @@ import _isNil from 'lodash/isNil';
import { loginWithToken } from 'pawchat-shared/model/user';
import { getUserJWT } from '../../utils/jwt-helper';
import { useHistory } from 'react-router';
import { SidebarContextProvider } from './SidebarContext';
/**
* hooks
@ -76,6 +77,10 @@ export const MainProvider: React.FC = React.memo((props) => {
return <div>, Store </div>;
}
return <ReduxProvider store={store}>{props.children}</ReduxProvider>;
return (
<ReduxProvider store={store}>
<SidebarContextProvider>{props.children}</SidebarContextProvider>
</ReduxProvider>
);
});
MainProvider.displayName = 'MainProvider';

@ -0,0 +1,38 @@
import React, { useContext, useState, useCallback } from 'react';
import _noop from 'lodash/noop';
interface SidebarContextProps {
showSidebar: boolean;
switchSidebar: () => void;
setShowSidebar: React.Dispatch<React.SetStateAction<boolean>>;
}
const SidebarContext = React.createContext<SidebarContextProps>({
showSidebar: true,
switchSidebar: _noop,
setShowSidebar: _noop,
});
SidebarContext.displayName = 'SidebarContext';
export const SidebarContextProvider: React.FC = React.memo((props) => {
const [showSidebar, setShowSidebar] = useState(true);
// 切换
const switchSidebar = useCallback(() => {
setShowSidebar(!showSidebar);
}, [showSidebar]);
return (
<SidebarContext.Provider
value={{ showSidebar, switchSidebar, setShowSidebar }}
>
{props.children}
</SidebarContext.Provider>
);
});
SidebarContextProvider.displayName = 'SidebarContextProvider';
export function useSidebarContext(): SidebarContextProps {
const context = useContext(SidebarContext);
return context;
}

@ -1,8 +1,7 @@
import React from 'react';
import { Content } from './Content';
import { MainContent } from './Content';
import { Navbar } from './Navbar';
import { MainProvider } from './Provider';
import { Sidebar } from './Sidebar';
export const MainRoute: React.FC = React.memo(() => {
return (
@ -10,9 +9,7 @@ export const MainRoute: React.FC = React.memo(() => {
<MainProvider>
<Navbar />
<Sidebar />
<Content />
<MainContent />
</MainProvider>
</div>
);

@ -1,3 +1,9 @@
{
"extends": "../tsconfig.json"
"extends": "../tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}

@ -9,7 +9,9 @@ import path from 'path';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import CopyPlugin from 'copy-webpack-plugin';
import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin';
delete process.env.TS_NODE_PROJECT; // https://github.com/dividab/tsconfig-paths-webpack-plugin/issues/32
require('../build/script/buildPublicTranslation.js'); // 编译前先执行一下构建翻译的脚本
const ROOT_PATH = path.resolve(__dirname, './');
@ -48,6 +50,7 @@ const config: Configuration = {
options: {
loader: 'tsx',
target: 'es2015',
tsconfigRaw: require('./tsconfig.json'),
},
},
{
@ -92,6 +95,11 @@ const config: Configuration = {
},
resolve: {
extensions: ['.tsx', '.ts', '.js', '.css'],
plugins: [
new TsconfigPathsPlugin({
configFile: path.resolve(ROOT_PATH, './tsconfig.json'),
}),
],
},
plugins: [
new DefinePlugin({

@ -3303,7 +3303,7 @@ engine.io-parser@~4.0.1:
dependencies:
base64-arraybuffer "0.1.4"
enhanced-resolve@^5.8.0:
enhanced-resolve@^5.7.0, enhanced-resolve@^5.8.0:
version "5.8.2"
resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.8.2.tgz#15ddc779345cbb73e97c611cd00c01c1e7bf4d8b"
integrity sha512-F27oB3WuHDzvR2DOGNTaYy0D5o0cnrv8TeI482VM4kYgQd/FT9lUQwuNsJ0oOHtBUq7eiW5ytqzp7nBFknL+GA==
@ -7574,6 +7574,11 @@ react-router@5.2.0, react-router@^5.2.0:
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
react-use-gesture@^9.1.3:
version "9.1.3"
resolved "https://registry.yarnpkg.com/react-use-gesture/-/react-use-gesture-9.1.3.tgz#92bd143e4f58e69bd424514a5bfccba2a1d62ec0"
integrity sha512-CdqA2SmS/fj3kkS2W8ZU8wjTbVBAIwDWaRprX7OKaj7HlGwBasGEFggmk5qNklknqk9zK/h8D355bEJFTpqEMg==
react@^17.0.2:
version "17.0.2"
resolved "https://registry.npmjs.org/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
@ -8940,6 +8945,15 @@ ts-node@^10.0.0:
source-map-support "^0.5.17"
yn "3.1.1"
tsconfig-paths-webpack-plugin@^3.5.1:
version "3.5.1"
resolved "https://registry.yarnpkg.com/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-3.5.1.tgz#e4dbf492a20dca9caab60086ddacb703afc2b726"
integrity sha512-n5CMlUUj+N5pjBhBACLq4jdr9cPTitySCjIosoQm0zwK99gmrcTGAfY9CwxRFT9+9OleNWXPRUcxsKP4AYExxQ==
dependencies:
chalk "^4.1.0"
enhanced-resolve "^5.7.0"
tsconfig-paths "^3.9.0"
tsconfig-paths@^3.9.0:
version "3.9.0"
resolved "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz#098547a6c4448807e8fcb8eae081064ee9a3c90b"

Loading…
Cancel
Save