Compare commits

...

8 Commits

43 changed files with 650 additions and 60 deletions

7
.dockerignore Normal file
View File

@ -0,0 +1,7 @@
.vscode/
node_modules/
npm-debug.log
dist/
build/
test/
yarn-error.log

4
.prettierignore Normal file
View File

@ -0,0 +1,4 @@
node_modules
build
dist
coverage

6
.prettierrc.json Normal file
View File

@ -0,0 +1,6 @@
{
"trailingComma": "es5",
"tabWidth": 4,
"semi": false,
"singleQuote": true
}

7
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,7 @@
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"[dockerfile]": {
"editor.defaultFormatter": "ms-azuretools.vscode-docker"
}
}

31
Dockerfile Normal file
View File

@ -0,0 +1,31 @@
# Build application
FROM cubetiq/calpine-node AS builder
RUN apk update && \
# For build commit hash in "process.env.COMMIT_ID"
apk add git && \
apk add tzdata && \
cp /usr/share/zoneinfo/Asia/Phnom_Penh /etc/localtime && \
echo "Asia/Phnom_Penh" > /etc/timezone
WORKDIR /app
COPY package.json ./
# Set custom registry for npm registry (from cubetiq local server)
RUN yarn config set registry https://r.ctdn.net
RUN yarn
COPY . .
RUN yarn build
# Clean up unused packages
RUN apk del tzdata && \
apk del git
# Build production image
FROM nginx:alpine
LABEL maintainer="sombochea@cubetiqs.com"
WORKDIR /usr/share/nginx/html
COPY --from=builder /app/build/ /usr/share/nginx/html
COPY --from=builder /app/docker/nginx.conf /etc/nginx/conf.d
RUN rm /etc/nginx/conf.d/default.conf
EXPOSE 80

View File

@ -1,10 +1,14 @@
# CUBETIQ React App
CUBETIQ React App Template for general use in react application.
# Libraries
- Create React App: 4.0.3
- React: 17.0.0
- TypeScript: 4.1.2
- Craco: 6.1.2 | Craco less: 1.17.1
# Contributors
- Sambo Chea <sombochea@cubetiqs.com>
- Sambo Chea <sombochea@cubetiqs.com>

50
craco.config.js Normal file
View File

@ -0,0 +1,50 @@
const path = require('path')
const webpack = require('webpack')
const npmPackage = require('./package.json')
const { v4: uuidv4 } = require('uuid')
const { execSync } = require('child_process')
// Get the last commit id/log
const gitFetchCommitIdCommand = 'git rev-parse HEAD'
// Execute the command
const fetchGitCommitId = () => {
try {
return execSync(gitFetchCommitIdCommand).toString().trim()
} catch (e) {
console.error(e)
return '-1'
}
}
// get current date (build date time)
const today = new Date()
// get date as version suffix: 10112021
const dateVersion = `${today.getDay()}${today.getMonth()}${today.getFullYear()}`
module.exports = {
webpack: {
alias: {
'@': path.resolve(__dirname, 'src/'),
},
plugins: {
add: [
new webpack.DefinePlugin({
'process.env.PACKAGE_NAME': `"${npmPackage.name}"`,
'process.env.PACKAGE_VERSION': `"${dateVersion}"`,
'process.env.BUILD_NUMBER': `"${uuidv4()}"`,
'process.env.BUILD_DATE': `"${today.toLocaleString()}"`,
'process.env.COMMIT_ID': `"${fetchGitCommitId()}"`,
}),
],
},
},
jest: {
configure: {
moduleNameMapper: {
'^@(.*)$': '<rootDir>/src$1',
},
},
},
}

21
docker/nginx.conf Normal file
View File

@ -0,0 +1,21 @@
server {
listen 80;
# serve static files
location ~ ^/(images|javascript|js|css|flash|media|static)/ {
root /usr/share/nginx/html;
expires 30d;
}
# serve default location
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}

View File

@ -1,27 +1,28 @@
{
"name": "cubetiq-react-app",
"version": "0.1.0",
"author": {
"name": "Sambo Chea",
"email": "sombochea@cubetiqs.com",
"url": "https://github.com/SomboChea"
},
"license": "MIT",
"repository": {
"url": "https://git.cubetiqs.com/CUBETIQ/cubetiq-react-app.git"
},
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"@types/jest": "^26.0.15",
"@types/node": "^12.0.0",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.3",
"typescript": "^4.1.2",
"web-vitals": "^1.0.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
"start": "craco start",
"build": "craco build",
"test": "craco test",
"eject": "react-scripts eject",
"format": "prettier --write ."
},
"eslintConfig": {
"extends": [
@ -40,5 +41,20 @@
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@craco/craco": "^6.4.3",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"@types/jest": "^26.0.15",
"@types/node": "^12.0.0",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"@types/react-router-dom": "^5.1.7",
"craco-less": "^2.0.0",
"prettier": "^2.3.1",
"react-scripts": "5.0.0",
"typescript": "^4.5.4"
}
}

View File

@ -1,20 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="%REACT_APP_DESCRIPTION%" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>%REACT_APP_NAME%</title>
</head>
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="%REACT_APP_DESCRIPTION%" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>%REACT_APP_NAME%</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

9
run-in-docker.sh Executable file
View File

@ -0,0 +1,9 @@
#!/bin/sh -e
echo "Building docker image..."
docker build . -t myreactapp:latest
echo "Running docker image..."
docker run --cpus=".1" --memory="5m" --rm -it -p 3003:3000 myreactapp:latest
# Now you can access the app at http://localhost:3003

View File

@ -1,10 +0,0 @@
import React from 'react';
import './App.css';
export default function App() {
return (
<div className="App">
<h1>CUBETIQ</h1>
</div>
);
}

5
src/app.config.ts Normal file
View File

@ -0,0 +1,5 @@
export const APP_NAME = process.env.PACKAGE_NAME
export const APP_VERSION = process.env.PACKAGE_VERSION
export const APP_BUILD_NUMBER = process.env.BUILD_NUMBER
export const APP_BUILD_DATE = process.env.BUILD_DATE
export const APP_COMMIT_ID = process.env.COMMIT_ID

View File

@ -1,3 +1,3 @@
.App {
text-align: center;
}
}

11
src/app/App.tsx Normal file
View File

@ -0,0 +1,11 @@
import AppProvider from '@/provider/AppProvider'
import RouterView from '../routes'
import './App.less'
const App = () => {
return (
<AppProvider><RouterView /></AppProvider>
)
}
export default App

5
src/config/index.ts Normal file
View File

@ -0,0 +1,5 @@
export const AppConfig = {
APP_NAME: process.env.REACT_APP_NAME,
CLIENT_TOKEN_KEY: "client_token",
}

131
src/context/AuthContext.tsx Normal file
View File

@ -0,0 +1,131 @@
import { AppConfig } from '@/config'
import { printError, printInfo } from '@/utils/log_util'
import { getStorage, setStorage } from '@/utils/ls_util'
import React, { useContext, useCallback, useReducer, useEffect } from 'react'
interface AuthContextState {
login: (args: { username: string; password: string }) => Promise<void>
state: {
user: {
username: string
userId: string
} | null
}
getToken: () => string | undefined | null
logout: () => void
isLogin: () => boolean
}
enum AuthActionType {
'LOGIN' = 'LOGIN',
'LOGOUT' = 'LOGOUT',
}
const AuthReducer: (
state: AuthContextState['state'],
action: {
type: AuthActionType
payload?: any
}
) => AuthContextState['state'] = (state, action) => {
switch (action.type) {
case AuthActionType.LOGIN:
return {
...state,
user: action.payload,
}
case AuthActionType.LOGOUT:
return {
...state,
user: null,
}
default:
return state
}
}
const AuthContext = React.createContext<AuthContextState>({
login: async () => {},
state: {
user: null,
},
getToken: () => {
return undefined
},
logout: () => {},
isLogin: () => { return false },
})
const AuthProvider: React.FC = (props) => {
const [state, dispatch] = useReducer(AuthReducer, {
user: null,
})
const login = async (args: { username: string; password: string }) => {
const res = {
data: {
token: 'DEMO_HAH',
},
}
setStorage(AppConfig.CLIENT_TOKEN_KEY, res.data.token)
await verify()
dispatch({
type: AuthActionType.LOGIN,
payload: args,
})
return
}
useEffect(() => {
const doVerify = async () => {
const token = getStorage(AppConfig.CLIENT_TOKEN_KEY)
const userAuthInfo = {}
printInfo("User details =>", token, userAuthInfo)
}
doVerify()
}, [])
const verify = async () => {
const token = getStorage(AppConfig.CLIENT_TOKEN_KEY)
printError('Verify not implemented with token: ', token)
const userAuthInfo = {}
return userAuthInfo
}
const logout = useCallback(() => {
setStorage(AppConfig.CLIENT_TOKEN_KEY, undefined)
dispatch({
type: AuthActionType.LOGOUT,
})
}, [])
const getToken = () => getStorage(AppConfig.CLIENT_TOKEN_KEY)
const isLogin = (): boolean => {
return getToken() != null
}
return (
<AuthContext.Provider
value={{
login,
state,
logout,
isLogin,
getToken,
}}
>
{props.children}
</AuthContext.Provider>
)
}
export const useAuthContext = () => {
return useContext(AuthContext)
}
export default AuthProvider

View File

@ -5,4 +5,4 @@ body {
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
}

View File

@ -1,17 +1,17 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import React from 'react'
import ReactDOM from 'react-dom'
import './index.less'
import App from '@/app/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();
reportWebVitals()

View File

@ -0,0 +1,4 @@
.about-title {
text-align: center;
font-weight: bold;
}

View File

@ -0,0 +1,9 @@
import './index.less'
export default function About() {
return (
<div>
<h1 className="about-title">About Us</h1>
</div>
)
}

7
src/pages/Error/403.tsx Normal file
View File

@ -0,0 +1,7 @@
export default function AccessDenied() {
return (
<div>
<h1>Access denied!</h1>
</div>
)
}

7
src/pages/Error/404.tsx Normal file
View File

@ -0,0 +1,7 @@
export default function NotFound() {
return (
<div>
<h1>Not found!</h1>
</div>
)
}

View File

@ -0,0 +1,5 @@
.home-title {
text-align: center;
font-weight: bold;
font-size: large;
}

9
src/pages/Home/index.tsx Normal file
View File

@ -0,0 +1,9 @@
import './index.less'
export default function Home() {
return (
<div>
<h1 className="home-title">Home</h1>
</div>
)
}

View File

25
src/pages/Info/index.tsx Normal file
View File

@ -0,0 +1,25 @@
import {
APP_BUILD_DATE,
APP_BUILD_NUMBER,
APP_COMMIT_ID,
APP_NAME,
APP_VERSION,
} from '@/app.config'
import { RouteTypes } from '@/routes/types'
import './index.less'
export default function Info() {
return (
<div>
<h1>App Info</h1>
<p>App name: {APP_NAME}</p>
<p>App version: {APP_VERSION}</p>
<p>App build number: {APP_BUILD_NUMBER}</p>
<p>App build date: {APP_BUILD_DATE}</p>
<p> App commit id: {APP_COMMIT_ID}</p>
<br />
<a href={RouteTypes.RESET}>Reset</a>
</div>
)
}

View File

@ -0,0 +1,4 @@
.login-title {
text-align: center;
font-weight: bold;
}

View File

@ -0,0 +1,9 @@
import './index.less'
export default function Login() {
return (
<div>
<h1 className="login-title">Login</h1>
</div>
)
}

View File

@ -0,0 +1,7 @@
export default function Profile() {
return (
<div>
<h1>Profile</h1>
</div>
)
}

23
src/pages/Reset/index.tsx Normal file
View File

@ -0,0 +1,23 @@
import { RouteTypes } from '@/routes/types'
import { clearStorage } from '@/utils/ls_util'
import { useEffect } from 'react'
import { Redirect } from 'react-router'
const Reset = () => {
const clearAllCaches = () => {
caches.keys().then((cacheNames) => {
cacheNames.forEach((cacheName) => {
caches.delete(cacheName).then((r) => console.log('Caches cleared', r))
})
})
}
useEffect(() => {
clearStorage()
sessionStorage.clear()
clearAllCaches()
}, [])
return <Redirect to={RouteTypes.HOME} />
}
export default Reset

View File

@ -0,0 +1,5 @@
const AppProvider = (props: any) => {
return <>{props.children}</>
}
export default AppProvider

View File

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

21
src/routes/AuthRoute.tsx Normal file
View File

@ -0,0 +1,21 @@
import { useAuthContext } from '@/context/AuthContext'
import AccessDenied from '@/pages/Error/403'
import { Route } from 'react-router-dom'
const AuthRoute = (props: any) => {
const { component, ...rest } = props
const { isLogin } = useAuthContext()
if (!isLogin()) {
return (<AccessDenied />)
}
return (
<Route
{...rest}
render={component}
/>
)
}
export default AuthRoute

36
src/routes/index.tsx Normal file
View File

@ -0,0 +1,36 @@
import AuthProvider from '@/context/AuthContext'
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'
import AuthRoute from './AuthRoute'
import { routes } from './routes'
const RouterView = () => {
return (
<Router>
<Switch>
{routes.map((route) => {
const { withAuthority } = route
if (withAuthority) {
return (
<AuthProvider key={route.key}>
<AuthRoute {...route} />
</AuthProvider>
)
} else {
return (
<Route
key={route.key}
exact={route.exact}
component={route.component}
children={route.children}
location={route.location}
path={route.path}
/>
)
}
})}
</Switch>
</Router>
)
}
export default RouterView

27
src/routes/interfaces.ts Normal file
View File

@ -0,0 +1,27 @@
import { RouteProps } from 'react-router-dom'
export interface Authority {
authority?: string | undefined
strict?: boolean | undefined
with?: Authority | undefined
}
export interface AuthorityProps {
withAuthority?: boolean | undefined
authorities?: Authority | string[] | string | undefined
strictAuthority?: boolean | undefined
}
export interface CustomRouteProps extends RouteProps, AuthorityProps {
key: string
}
export interface MenuProps {
icon: any
label?: string | undefined
}
export interface SideSubMenuProps extends MenuProps {
key?: string | undefined
subMenus: SideSubMenuProps[]
}

57
src/routes/routes.tsx Normal file
View File

@ -0,0 +1,57 @@
import About from '@/pages/About'
import NotFound from '@/pages/Error/404'
import Home from '@/pages/Home'
import Info from '@/pages/Info'
import Login from '@/pages/Login'
import Profile from '@/pages/Profile'
import Reset from '@/pages/Reset'
import { CustomRouteProps } from '@/routes/interfaces'
import { RouteTypes } from '@/routes/types'
const routes: CustomRouteProps[] = [
// Auth
{
key: 'login',
path: RouteTypes.LOGIN,
component: () => <Login />,
},
{
key: 'home',
exact: true,
path: RouteTypes.HOME,
component: () => <Home />,
},
{
key: 'about',
path: RouteTypes.ABOUT,
component: () => <About />,
},
{
exact: true,
key: 'info',
path: RouteTypes.INFO,
component: () => <Info />,
},
{
exact: true,
key: 'reset',
path: RouteTypes.RESET,
component: () => <Reset />,
},
{
exact: true,
key: 'profile',
path: RouteTypes.PROFILE,
component: () => <Profile />,
withAuthority: true,
},
// Errors
{
key: 'notfound',
path: RouteTypes.ERROR_404,
component: () => <NotFound />,
},
]
export { routes }

17
src/routes/types.ts Normal file
View File

@ -0,0 +1,17 @@
const RouteTypes = {
HOME: "/",
ABOUT: "/about",
PROFILE: "/profile",
INFO: "/info",
RESET: "/reset",
// Auth
LOGIN: '/login',
// Errors
ERROR_404: "**",
}
export {
RouteTypes,
}

View File

@ -2,4 +2,4 @@
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
import '@testing-library/jest-dom'

3
src/utils/log_util.ts Normal file
View File

@ -0,0 +1,3 @@
export const printInfo = (message?: any, ...opts: any[]) => console.log(message, opts)
export const printError = (message?: any, ...opts: any[]) => console.error(message, opts)
export const printWarn = (message?: any, ...opts: any[]) => console.warn(message, opts)

11
src/utils/ls_util.ts Normal file
View File

@ -0,0 +1,11 @@
export const setStorage = (key: string, value?: any) => {
localStorage.setItem(key, JSON.stringify(value))
}
export const getStorage = (key: string, defaultValue?: string): (string | undefined | null) => {
return localStorage.getItem(key) ?? defaultValue
}
export const clearStorage = () => {
return localStorage.clear()
}

View File

@ -20,6 +20,7 @@
"noEmit": true,
"jsx": "react-jsx"
},
"extends": "./tsconfig.paths.json",
"include": [
"src"
]

8
tsconfig.paths.json Normal file
View File

@ -0,0 +1,8 @@
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
}
}
}