diff --git a/recipes/_ts/basic/config.js b/recipes/_ts/basic/config.js new file mode 100644 index 0000000..c80fad9 --- /dev/null +++ b/recipes/_ts/basic/config.js @@ -0,0 +1,71 @@ +const getConfig = () => { + return { + bundler: "webpack", + ui: "material-ui", + state: "none", + buildDir: "public", + sourceDir: { + main: "src", + static: "static", + assets: "assets", + images: "images", + containers: "modules", + i18n: "i18n", + components: "components", + businessLogic: "blocks", + userInterface: "ui", + utility: "utils", + hooks: "hooks", + theme: "theme", + store: "store", + services: "services", + locales: "translations", + }, + modules: { + signIn: "SignIn", + dashboard: "Dashboard", + notFound: "NotFound", + }, + canAdd: { + gitIgnore: true, + eslint: true, + environment: true, + prettier: true, + husky: true, + hooks: true, + hookForm: true, + routes: true, + utils: true, + static: true, + i18n: true, + modules: true, + componentsCopy: false, + fullComponents: true, + }, + }; +}; + +const getModulesList = () => { + return [ + "reactWithI18n", + "router", + "services", + "utils" + ]; +}; + +const getDevModulesList = () => { + return [ + "webpack", + "webpackPlugins", + "webpackLoaders", + "babel", + "basicTypescriptDev" + ]; +}; + +module.exports = { + getConfig, + getModulesList, + getDevModulesList, +}; diff --git a/recipes/_ts/basic/snippets/.babelrc b/recipes/_ts/basic/snippets/.babelrc new file mode 100644 index 0000000..a3feffc --- /dev/null +++ b/recipes/_ts/basic/snippets/.babelrc @@ -0,0 +1,14 @@ +{ + "presets": [ + [ + "@babel/preset-env", + { + "targets": { + "node": "current" + } + } + ], + "@babel/preset-react", + "@babel/preset-typescript" + ] +} \ No newline at end of file diff --git a/recipes/_ts/basic/snippets/.eslintrc.json b/recipes/_ts/basic/snippets/.eslintrc.json new file mode 100644 index 0000000..c4f6a88 --- /dev/null +++ b/recipes/_ts/basic/snippets/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["airbnb-base", "prettier"] +} \ No newline at end of file diff --git a/recipes/_ts/basic/snippets/.prettierrc.json b/recipes/_ts/basic/snippets/.prettierrc.json new file mode 100644 index 0000000..03207f6 --- /dev/null +++ b/recipes/_ts/basic/snippets/.prettierrc.json @@ -0,0 +1,20 @@ +{ + "arrowParens": "always", + "bracketSpacing": true, + "embeddedLanguageFormatting": "auto", + "htmlWhitespaceSensitivity": "css", + "insertPragma": false, + "jsxBracketSameLine": false, + "jsxSingleQuote": false, + "printWidth": 100, + "proseWrap": "preserve", + "quoteProps": "as-needed", + "requirePragma": false, + "semi": true, + "singleQuote": false, + "tabWidth": 2, + "trailingComma": "all", + "useTabs": false, + "vueIndentScriptAndStyle": false, + "plugins": ["prettier-plugin-tailwindcss"] +} diff --git a/recipes/_ts/basic/snippets/index.js b/recipes/_ts/basic/snippets/index.js new file mode 100644 index 0000000..2a1fad1 --- /dev/null +++ b/recipes/_ts/basic/snippets/index.js @@ -0,0 +1,45 @@ +const shell = require('shelljs') +const { getWebPackConfig } = require('./webpack.config') +const rootIndex = require('./sources/index') +const rootApp = require('./sources/App') +const rootRoutes = require('./sources/Routes') +const withI18n = require('./sources/withI18n') +const signInModule = require('./sources/SignIn') +const dashboardModule = require('./sources/Dashboard') +const pageLoaderBlock = require('./sources/PageLoader') +const sidebarBlock = require('./sources/Sidebar') +const topBarBlock = require('./sources/TopBar') + +const sourceCodes = { + index: rootIndex, + App: rootApp, + Routes: rootRoutes, + withI18n: withI18n, + SignIn: signInModule, + Dashboard: dashboardModule, + PageLoader: pageLoaderBlock, + Sidebar: sidebarBlock, + TopBar: topBarBlock, + } + + +const getFileContent = (fileName) => { + return shell.cat(`${__dirname}/${fileName}`) +} + +const getDynamicSourceCode = (fileName, appName, baseConfig) => { + const key = fileName.replace(/\.(js|ts|tsx)$/, '') + const source = sourceCodes[key] + + if (!source) { + throw new Error(`Source template not found: ${key}`) + } + + return source.getSourceCode(appName, baseConfig) +} + +module.exports = { + getFileContent, + getWebPackConfig, + getDynamicSourceCode, +} diff --git a/recipes/_ts/basic/snippets/sources/App.js b/recipes/_ts/basic/snippets/sources/App.js new file mode 100644 index 0000000..b0b7726 --- /dev/null +++ b/recipes/_ts/basic/snippets/sources/App.js @@ -0,0 +1,35 @@ +function getSourceCode(appName, { sourceDir }) { + return `import React, { FC } from 'react' +import { unstable_HistoryRouter as HistoryRouter } from 'react-router-dom' +import { createBrowserHistory, History } from 'history' + +import Routes from './Routes' + +// Block Components +import { ErrorHandler, PageLoader } from '@/components/blocks' + +import { withTranslation } from '@/${sourceDir.i18n}' + +const browserHistory = createBrowserHistory() + +interface AppProps extends WithTranslation {} + +function App(props: AppProps) { + return ( + + + + + + + ) +} + +export default withTranslation(App) + +` +} + +module.exports = { + getSourceCode, +}; \ No newline at end of file diff --git a/recipes/_ts/basic/snippets/sources/Dashboard.js b/recipes/_ts/basic/snippets/sources/Dashboard.js new file mode 100644 index 0000000..e60992a --- /dev/null +++ b/recipes/_ts/basic/snippets/sources/Dashboard.js @@ -0,0 +1,61 @@ +function getSourceCode(appName, { sourceDir }) { +return `import React from 'react' +import { useNavigate } from 'react-router-dom' + +import { I18nMsg } from '@/${sourceDir.i18n}' + +// Utils. +import { RoutePaths } from '@/${sourceDir.utility}' + +// Service Hooks +import useFetch from '@/${sourceDir.hooks}/useFetch' + +const SAMPLE_GET_API_URL = 'https://jsonplaceholder.typicode.com/users' + +interface DashboardProps { + title?: string +} + +interface User { + id: number + name: string +} + +const Dashboard: React.FC = (props) => { + const navigate = useNavigate() + + const { loading, error, response = [] } = useFetch(SAMPLE_GET_API_URL) + + if (loading) return 'Loading..' + if (error) return error.message + + return ( + <> +
+
+

+ goes here +

+ {response.map((user) => { + return
  • {user.name}
  • + })} + +
    +
    + + ) +} + +export default Dashboard +` +} + +module.exports = { + getSourceCode, +}; \ No newline at end of file diff --git a/recipes/_ts/basic/snippets/sources/PageLoader.js b/recipes/_ts/basic/snippets/sources/PageLoader.js new file mode 100644 index 0000000..546771e --- /dev/null +++ b/recipes/_ts/basic/snippets/sources/PageLoader.js @@ -0,0 +1,21 @@ +function getSourceCode(appName, { sourceDir }) { +return `import React, { Fragment } from 'react' + +// UI Components. +import { Spinner } from '@/components/ui' + +interface PageLoaderProps { + loading: boolean +} + +export default function PageLoader({ loading }: PageLoaderProps) { + return loading ? : null +} + +` + +} + +module.exports = { + getSourceCode, +}; \ No newline at end of file diff --git a/recipes/_ts/basic/snippets/sources/Routes.js b/recipes/_ts/basic/snippets/sources/Routes.js new file mode 100644 index 0000000..ad2ef2f --- /dev/null +++ b/recipes/_ts/basic/snippets/sources/Routes.js @@ -0,0 +1,42 @@ +function getSourceCode(appName, { modules }) { + return `import React, { Suspense } from 'react' +import { Route, Routes } from 'react-router-dom' + +// Block Components +import { PageLoader } from '@/components/blocks' + +// Utils +import { RoutePaths } from '@/utils' + +// Lazy-loaded modules +const SignInModule = React.lazy(() => + import(/* webpackChunkName: "${modules.signIn}" */ '@/modules/${modules.signIn}') +) + +const DashBoardModule = React.lazy(() => + import(/* webpackChunkName: "${modules.dashboard}" */ '@/modules/${modules.dashboard}') +) + +const NotFoundModule = React.lazy(() => + import(/* webpackChunkName: "${modules.notFound}" */ '@/modules/${modules.notFound}') +) + +function RoutesComponent() { + return ( + }> + + } /> + } /> + } /> + + + ) +} + +export default RoutesComponent +` +} + +module.exports = { + getSourceCode, +} diff --git a/recipes/_ts/basic/snippets/sources/Sidebar.js b/recipes/_ts/basic/snippets/sources/Sidebar.js new file mode 100644 index 0000000..f0629da --- /dev/null +++ b/recipes/_ts/basic/snippets/sources/Sidebar.js @@ -0,0 +1,57 @@ +function getSourceCode(appName, { sourceDir }) { +return `import React, { HTMLAttributes } from 'react' +import { useI18n } from '@/${sourceDir.i18n}' + +// Domain Components. +import SidebarNav from './SidebarNav' + +// Utils. +import { RoutePaths } from '@/${sourceDir.utility}' + +interface Page { + title: string + href: string + icon: string +} + +interface SidebarProps extends HTMLAttributes { + open: boolean + variant: string + onClose?: () => void + className?: string +} + +const Sidebar: React.FC = (props) => { + const { open, variant, onClose, className, ...rest } = props + const { formatMessage } = useI18n() + + const pages: Page[] = [ + { + title: formatMessage({ id: 'dashboard' }), + href: RoutePaths.dashboard, + icon: '', + }, + { + title: formatMessage({ id: 'other_module' }), + href: RoutePaths.dashboard, + icon: '', + }, + ] + + return ( +
    +
    + +
    +
    + ) +} + +export default Sidebar +` + +} + +module.exports = { + getSourceCode, +}; \ No newline at end of file diff --git a/recipes/_ts/basic/snippets/sources/SignIn.js b/recipes/_ts/basic/snippets/sources/SignIn.js new file mode 100644 index 0000000..c003b75 --- /dev/null +++ b/recipes/_ts/basic/snippets/sources/SignIn.js @@ -0,0 +1,88 @@ +function getSourceCode(appName, { sourceDir }) { +return `import React from 'react' +import { useNavigate } from 'react-router-dom' + +// UI Components. +import { InputTextField } from '@/components/ui' + +// Custom Hooks. +import { useI18n } from '@/${sourceDir.i18n}' + +// Utils. +import { RoutePaths } from '@/${sourceDir.utility}' + +// Service Hooks +import usePost from '@/${sourceDir.hooks}/usePost' + +interface SignInProps { + title?: string +} + +interface PostData { + id: number + title: string + body: string + userId: number +} + +interface PostResponse { + id: number + title: string + body: string + userId: number +} + +const SAMPLE_POST_API_URL = 'https://jsonplaceholder.typicode.com/posts' + +const SignIn: React.FC = (props) => { + const navigate = useNavigate() + const { formatMessage } = useI18n() + + const { loading, error, response, sendPostData } = usePost( + SAMPLE_POST_API_URL, + 'sendPostData' + ) + + return ( + <> +
    +
    +

    Login Form

    +
    + {error && 'API is failed'} + {response && 'Submitted successfully'} +
    + Username: + + +
    +
    + + ) +} + +export default SignIn +` + +} + +module.exports = { + getSourceCode, +}; diff --git a/recipes/_ts/basic/snippets/sources/TopBar.js b/recipes/_ts/basic/snippets/sources/TopBar.js new file mode 100644 index 0000000..ad88e81 --- /dev/null +++ b/recipes/_ts/basic/snippets/sources/TopBar.js @@ -0,0 +1,30 @@ +function getSourceCode(appName, { sourceDir }) { +return `import React, { HTMLAttributes } from 'react' +import { Link as RouterLink } from 'react-router-dom' + +import { I18nMsg } from '@/${sourceDir.i18n}' + +interface TopBarProps extends HTMLAttributes { + className?: string + onSidebarOpen?: () => void +} + +const TopBar: React.FC = (props) => { + const { className, ...rest } = props + + return ( +
    + + + +
    + ) +} + +export default TopBar +` +} + +module.exports = { + getSourceCode, +}; \ No newline at end of file diff --git a/recipes/_ts/basic/snippets/sources/components/blocks/ErrorHandler/ErrorHandler.tsx b/recipes/_ts/basic/snippets/sources/components/blocks/ErrorHandler/ErrorHandler.tsx new file mode 100644 index 0000000..2add128 --- /dev/null +++ b/recipes/_ts/basic/snippets/sources/components/blocks/ErrorHandler/ErrorHandler.tsx @@ -0,0 +1,47 @@ +import React, { Component, ReactNode, ErrorInfo } from 'react' + +interface ErrorBoundaryProps { + children: ReactNode +} + +interface ErrorBoundaryState { + error: Error | null + errorInfo: ErrorInfo | null +} + +export default class ErrorBoundary extends Component< + ErrorBoundaryProps, + ErrorBoundaryState +> { + constructor(props: ErrorBoundaryProps) { + super(props) + this.state = { error: null, errorInfo: null } + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo) { + // Catch errors in any components below and re-render with error message + this.setState({ + error: error, + errorInfo: errorInfo, + }) + // You can also log error messages to an error reporting service here + } + + render() { + if (this.state.errorInfo) { + // Error path + return ( +
    +

    Something went wrong.

    +
    + {this.state.error && this.state.error.toString()} +
    + {this.state.errorInfo.componentStack} +
    +
    + ) + } + // Normally, just render children + return this.props.children + } +} diff --git a/recipes/_ts/basic/snippets/sources/components/blocks/ErrorHandler/index.ts b/recipes/_ts/basic/snippets/sources/components/blocks/ErrorHandler/index.ts new file mode 100644 index 0000000..55ed726 --- /dev/null +++ b/recipes/_ts/basic/snippets/sources/components/blocks/ErrorHandler/index.ts @@ -0,0 +1 @@ +export { default } from './ErrorHandler' diff --git a/recipes/_ts/basic/snippets/sources/components/blocks/Loader/PageLoader/index.ts b/recipes/_ts/basic/snippets/sources/components/blocks/Loader/PageLoader/index.ts new file mode 100644 index 0000000..681ba20 --- /dev/null +++ b/recipes/_ts/basic/snippets/sources/components/blocks/Loader/PageLoader/index.ts @@ -0,0 +1 @@ +export { default } from './PageLoader' diff --git a/recipes/_ts/basic/snippets/sources/components/blocks/Loader/index.ts b/recipes/_ts/basic/snippets/sources/components/blocks/Loader/index.ts new file mode 100644 index 0000000..715d2d1 --- /dev/null +++ b/recipes/_ts/basic/snippets/sources/components/blocks/Loader/index.ts @@ -0,0 +1 @@ +export { default as PageLoader } from './PageLoader' diff --git a/recipes/_ts/basic/snippets/sources/components/blocks/Region/Footer/Footer.tsx b/recipes/_ts/basic/snippets/sources/components/blocks/Region/Footer/Footer.tsx new file mode 100644 index 0000000..6ecbd3d --- /dev/null +++ b/recipes/_ts/basic/snippets/sources/components/blocks/Region/Footer/Footer.tsx @@ -0,0 +1,24 @@ +import React, { HTMLAttributes } from 'react' + +interface FooterProps extends HTMLAttributes { + className?: string +} + +const Footer: React.FC = (props) => { + const { className, ...rest } = props + + return ( +
    +
    + ©{' '} + + React Chef + + . 2020 +
    +
    React Chef
    +
    + ) +} + +export default Footer diff --git a/recipes/_ts/basic/snippets/sources/components/blocks/Region/Footer/index.ts b/recipes/_ts/basic/snippets/sources/components/blocks/Region/Footer/index.ts new file mode 100644 index 0000000..5d06e9b --- /dev/null +++ b/recipes/_ts/basic/snippets/sources/components/blocks/Region/Footer/index.ts @@ -0,0 +1 @@ +export { default } from './Footer' diff --git a/recipes/_ts/basic/snippets/sources/components/blocks/Region/Sidebar/SidebarNav/SidebarNav.tsx b/recipes/_ts/basic/snippets/sources/components/blocks/Region/Sidebar/SidebarNav/SidebarNav.tsx new file mode 100644 index 0000000..5179243 --- /dev/null +++ b/recipes/_ts/basic/snippets/sources/components/blocks/Region/Sidebar/SidebarNav/SidebarNav.tsx @@ -0,0 +1,28 @@ +import React, { HTMLAttributes } from 'react' + +interface Page { + title: string + href?: string + icon?: string +} + +interface SidebarNavProps extends HTMLAttributes { + className?: string + pages: Page[] +} + +const SidebarNav: React.FC = (props) => { + const { pages, className, ...rest } = props + + return ( +
      + {pages.map((page) => ( +
    • + {page.title} +
    • + ))} +
    + ) +} + +export default SidebarNav diff --git a/recipes/_ts/basic/snippets/sources/components/blocks/Region/Sidebar/SidebarNav/index.ts b/recipes/_ts/basic/snippets/sources/components/blocks/Region/Sidebar/SidebarNav/index.ts new file mode 100644 index 0000000..bd8db7e --- /dev/null +++ b/recipes/_ts/basic/snippets/sources/components/blocks/Region/Sidebar/SidebarNav/index.ts @@ -0,0 +1 @@ +export { default } from './SidebarNav' diff --git a/recipes/_ts/basic/snippets/sources/components/blocks/Region/Sidebar/index.ts b/recipes/_ts/basic/snippets/sources/components/blocks/Region/Sidebar/index.ts new file mode 100644 index 0000000..877187c --- /dev/null +++ b/recipes/_ts/basic/snippets/sources/components/blocks/Region/Sidebar/index.ts @@ -0,0 +1 @@ +export { default } from './Sidebar' diff --git a/recipes/_ts/basic/snippets/sources/components/blocks/Region/TopBar/index.ts b/recipes/_ts/basic/snippets/sources/components/blocks/Region/TopBar/index.ts new file mode 100644 index 0000000..4f7373f --- /dev/null +++ b/recipes/_ts/basic/snippets/sources/components/blocks/Region/TopBar/index.ts @@ -0,0 +1 @@ +export { default } from './TopBar' diff --git a/recipes/_ts/basic/snippets/sources/components/blocks/Region/index.ts b/recipes/_ts/basic/snippets/sources/components/blocks/Region/index.ts new file mode 100644 index 0000000..0833b26 --- /dev/null +++ b/recipes/_ts/basic/snippets/sources/components/blocks/Region/index.ts @@ -0,0 +1,3 @@ +export { default as Footer } from './Footer' +export { default as Sidebar } from './Sidebar' +export { default as TopBar } from './TopBar' diff --git a/recipes/_ts/basic/snippets/sources/components/blocks/index.ts b/recipes/_ts/basic/snippets/sources/components/blocks/index.ts new file mode 100644 index 0000000..3a09345 --- /dev/null +++ b/recipes/_ts/basic/snippets/sources/components/blocks/index.ts @@ -0,0 +1,3 @@ +export { TopBar, Sidebar, Footer } from './Region' +export { PageLoader } from './Loader' +export { default as ErrorHandler } from './ErrorHandler' diff --git a/recipes/_ts/basic/snippets/sources/components/ui/Fields/InputTextField/InputTextField.tsx b/recipes/_ts/basic/snippets/sources/components/ui/Fields/InputTextField/InputTextField.tsx new file mode 100644 index 0000000..0106239 --- /dev/null +++ b/recipes/_ts/basic/snippets/sources/components/ui/Fields/InputTextField/InputTextField.tsx @@ -0,0 +1,36 @@ +import React, { ChangeEvent, FocusEvent } from 'react' + +interface InputTextFieldProps { + name: string + initialValue?: string + value?: string + placeholder?: string + handleChange: (event: ChangeEvent) => void + onBlur?: (event: FocusEvent) => void +} + +export default function InputTextField({ + name, + initialValue = '', + value, + placeholder, + handleChange, + onBlur, +}: InputTextFieldProps) { + const onChange = (event: ChangeEvent) => { + handleChange(event) + } + + return ( +
    + +
    + ) +} diff --git a/recipes/_ts/basic/snippets/sources/components/ui/Fields/InputTextField/index.ts b/recipes/_ts/basic/snippets/sources/components/ui/Fields/InputTextField/index.ts new file mode 100644 index 0000000..1f467a1 --- /dev/null +++ b/recipes/_ts/basic/snippets/sources/components/ui/Fields/InputTextField/index.ts @@ -0,0 +1 @@ +export { default } from './InputTextField' diff --git a/recipes/_ts/basic/snippets/sources/components/ui/Fields/index.ts b/recipes/_ts/basic/snippets/sources/components/ui/Fields/index.ts new file mode 100644 index 0000000..de2e702 --- /dev/null +++ b/recipes/_ts/basic/snippets/sources/components/ui/Fields/index.ts @@ -0,0 +1 @@ +export { default as InputTextField } from './InputTextField' diff --git a/recipes/_ts/basic/snippets/sources/components/ui/Loader/BlockLoader/BlockLoader.tsx b/recipes/_ts/basic/snippets/sources/components/ui/Loader/BlockLoader/BlockLoader.tsx new file mode 100644 index 0000000..ded7efd --- /dev/null +++ b/recipes/_ts/basic/snippets/sources/components/ui/Loader/BlockLoader/BlockLoader.tsx @@ -0,0 +1,7 @@ +import React from 'react' + +const BlockLoader = ({ props }) => { + return <>Loader goes here... +} + +export default BlockLoader diff --git a/recipes/_ts/basic/snippets/sources/components/ui/Loader/BlockLoader/index.ts b/recipes/_ts/basic/snippets/sources/components/ui/Loader/BlockLoader/index.ts new file mode 100644 index 0000000..2ebe7f4 --- /dev/null +++ b/recipes/_ts/basic/snippets/sources/components/ui/Loader/BlockLoader/index.ts @@ -0,0 +1 @@ +export { default } from './BlockLoader' diff --git a/recipes/_ts/basic/snippets/sources/components/ui/Loader/Spinner/Spinner.tsx b/recipes/_ts/basic/snippets/sources/components/ui/Loader/Spinner/Spinner.tsx new file mode 100644 index 0000000..59d3c0e --- /dev/null +++ b/recipes/_ts/basic/snippets/sources/components/ui/Loader/Spinner/Spinner.tsx @@ -0,0 +1,5 @@ +import React from 'react' + +export default function Spinner(props) { + return
    Spinner goes here...
    +} diff --git a/recipes/_ts/basic/snippets/sources/components/ui/Loader/Spinner/index.ts b/recipes/_ts/basic/snippets/sources/components/ui/Loader/Spinner/index.ts new file mode 100644 index 0000000..0be0188 --- /dev/null +++ b/recipes/_ts/basic/snippets/sources/components/ui/Loader/Spinner/index.ts @@ -0,0 +1 @@ +export { default } from './Spinner' diff --git a/recipes/_ts/basic/snippets/sources/components/ui/Loader/index.ts b/recipes/_ts/basic/snippets/sources/components/ui/Loader/index.ts new file mode 100644 index 0000000..5d4f2e0 --- /dev/null +++ b/recipes/_ts/basic/snippets/sources/components/ui/Loader/index.ts @@ -0,0 +1,2 @@ +export { default as Spinner } from './Spinner' +export { default as BlockLoader } from './BlockLoader' diff --git a/recipes/_ts/basic/snippets/sources/components/ui/index.ts b/recipes/_ts/basic/snippets/sources/components/ui/index.ts new file mode 100644 index 0000000..8cb7e22 --- /dev/null +++ b/recipes/_ts/basic/snippets/sources/components/ui/index.ts @@ -0,0 +1,2 @@ +export { Spinner, BlockLoader } from './Loader' +export { InputTextField } from './Fields' diff --git a/recipes/_ts/basic/snippets/sources/constants/index.ts b/recipes/_ts/basic/snippets/sources/constants/index.ts new file mode 100644 index 0000000..40e15e0 --- /dev/null +++ b/recipes/_ts/basic/snippets/sources/constants/index.ts @@ -0,0 +1 @@ +export { default as RoutePaths } from './route-paths' diff --git a/recipes/_ts/basic/snippets/sources/constants/route-paths.ts b/recipes/_ts/basic/snippets/sources/constants/route-paths.ts new file mode 100644 index 0000000..6965dad --- /dev/null +++ b/recipes/_ts/basic/snippets/sources/constants/route-paths.ts @@ -0,0 +1,5 @@ +export default { + SignIn: '/', + DashBoard: '/dashboard', + NotFound: '/not-found', +} diff --git a/recipes/_ts/basic/snippets/sources/env/dev.env b/recipes/_ts/basic/snippets/sources/env/dev.env new file mode 100644 index 0000000..e69de29 diff --git a/recipes/_ts/basic/snippets/sources/env/prod.env b/recipes/_ts/basic/snippets/sources/env/prod.env new file mode 100644 index 0000000..e69de29 diff --git a/recipes/_ts/basic/snippets/sources/env/qa.env b/recipes/_ts/basic/snippets/sources/env/qa.env new file mode 100644 index 0000000..e69de29 diff --git a/recipes/_ts/basic/snippets/sources/hooks/useFetch.tsx b/recipes/_ts/basic/snippets/sources/hooks/useFetch.tsx new file mode 100644 index 0000000..7158f8c --- /dev/null +++ b/recipes/_ts/basic/snippets/sources/hooks/useFetch.tsx @@ -0,0 +1,34 @@ +import { useState, useEffect } from 'react' +import axios, { AxiosError } from 'axios' + +interface UseFetchResult { + response: T | undefined + error: AxiosError | null + loading: boolean +} + +export default function useFetch(url: string): UseFetchResult { + const [response, setResponse] = useState(undefined) + const [error, setError] = useState(null) + const [loading, setLoading] = useState(false) + + useEffect(() => { + if (!url || url.trim() === '') return + + const fetchData = async () => { + try { + setLoading(true) + const result = await axios.get(url) + setResponse(result.data) + } catch (err) { + setError(err as AxiosError) + } finally { + setLoading(false) + } + } + + fetchData() + }, [url]) + + return { response, error, loading } +} diff --git a/recipes/_ts/basic/snippets/sources/hooks/useOnlineStatus.tsx b/recipes/_ts/basic/snippets/sources/hooks/useOnlineStatus.tsx new file mode 100644 index 0000000..e8aabe5 --- /dev/null +++ b/recipes/_ts/basic/snippets/sources/hooks/useOnlineStatus.tsx @@ -0,0 +1,26 @@ +import { useState, useEffect } from 'react' + +function useOnlineStatus(): boolean { + const [online, setOnline] = useState(window.navigator.onLine) + + useEffect(() => { + function handleOnline() { + setOnline(true) + } + function handleOffline() { + setOnline(false) + } + + window.addEventListener('online', handleOnline) + window.addEventListener('offline', handleOffline) + + return () => { + window.removeEventListener('online', handleOnline) + window.removeEventListener('offline', handleOffline) + } + }, []) + + return online +} + +export default useOnlineStatus diff --git a/recipes/_ts/basic/snippets/sources/hooks/usePost.tsx b/recipes/_ts/basic/snippets/sources/hooks/usePost.tsx new file mode 100644 index 0000000..410f2df --- /dev/null +++ b/recipes/_ts/basic/snippets/sources/hooks/usePost.tsx @@ -0,0 +1,33 @@ +import { useState } from 'react' +import axios, { AxiosError } from 'axios' + +interface UsePostResult { + response: T | undefined + error: AxiosError | null + loading: boolean + [key: string]: any +} + +export default function usePost( + url: string, + methodName: string = 'sendPostData' +): UsePostResult { + const [response, setResponse] = useState(undefined) + const [error, setError] = useState(null) + const [loading, setLoading] = useState(false) + + const postData = async (payload: P) => { + if (!url || url.trim() === '') return + try { + setLoading(true) + const result = await axios.post(url, payload) + setResponse(result.data) + } catch (err) { + setError(err as AxiosError) + } finally { + setLoading(false) + } + } + + return { response, error, loading, [methodName]: postData } +} diff --git a/recipes/_ts/basic/snippets/sources/i18n/index.ts b/recipes/_ts/basic/snippets/sources/i18n/index.ts new file mode 100644 index 0000000..7fba212 --- /dev/null +++ b/recipes/_ts/basic/snippets/sources/i18n/index.ts @@ -0,0 +1,10 @@ +import { useIntl, FormattedMessage, type IntlShape } from 'react-intl' + +// Re-export HOC with proper name +export { default as withTranslation } from './withI18n' + +// Typed hook alias +export const useI18n = (): IntlShape => useIntl() + +// Typed component alias +export const I18nMsg: typeof FormattedMessage = FormattedMessage diff --git a/recipes/_ts/basic/snippets/sources/index.js b/recipes/_ts/basic/snippets/sources/index.js new file mode 100644 index 0000000..b4b1c7a --- /dev/null +++ b/recipes/_ts/basic/snippets/sources/index.js @@ -0,0 +1,20 @@ +function getSourceCode(appName) { + return `import React from 'react' +import { createRoot } from 'react-dom/client' + +import App from './App' + +const container = document.getElementById('${appName}') + +if (!container) { + throw new Error('Root container not found') +} + +const root = createRoot(container) +root.render() +` +} + +module.exports = { + getSourceCode, +}; diff --git a/recipes/_ts/basic/snippets/sources/modules/Dashboard/index.ts b/recipes/_ts/basic/snippets/sources/modules/Dashboard/index.ts new file mode 100644 index 0000000..a7a1745 --- /dev/null +++ b/recipes/_ts/basic/snippets/sources/modules/Dashboard/index.ts @@ -0,0 +1 @@ +export { default } from './Dashboard' diff --git a/recipes/_ts/basic/snippets/sources/modules/NotFound/NotFound.tsx b/recipes/_ts/basic/snippets/sources/modules/NotFound/NotFound.tsx new file mode 100644 index 0000000..bcbebbf --- /dev/null +++ b/recipes/_ts/basic/snippets/sources/modules/NotFound/NotFound.tsx @@ -0,0 +1,21 @@ +import React from 'react' +import { useNavigate } from 'react-router-dom' + +// Utils. +import { RoutePaths } from '@/utils' + +const NotFound: React.FC = () => { + const navigate = useNavigate() + + return ( +
    +
    + +

    404 - Page not found

    + +
    +
    + ) +} + +export default NotFound diff --git a/recipes/_ts/basic/snippets/sources/modules/NotFound/index.ts b/recipes/_ts/basic/snippets/sources/modules/NotFound/index.ts new file mode 100644 index 0000000..2893216 --- /dev/null +++ b/recipes/_ts/basic/snippets/sources/modules/NotFound/index.ts @@ -0,0 +1 @@ +export { default } from './NotFound' diff --git a/recipes/_ts/basic/snippets/sources/modules/SignIn/index.ts b/recipes/_ts/basic/snippets/sources/modules/SignIn/index.ts new file mode 100644 index 0000000..9c9eb75 --- /dev/null +++ b/recipes/_ts/basic/snippets/sources/modules/SignIn/index.ts @@ -0,0 +1 @@ +export { default } from './SignIn' diff --git a/recipes/_ts/basic/snippets/sources/modules/index.ts b/recipes/_ts/basic/snippets/sources/modules/index.ts new file mode 100644 index 0000000..65a2fb9 --- /dev/null +++ b/recipes/_ts/basic/snippets/sources/modules/index.ts @@ -0,0 +1,3 @@ +export { default as SignIn } from './SignIn' +export { default as Dashboard } from './Dashboard' +export { default as NotFound } from './NotFound' diff --git a/recipes/_ts/basic/snippets/sources/static/images/Settings.svg b/recipes/_ts/basic/snippets/sources/static/images/Settings.svg new file mode 100644 index 0000000..7685cb0 --- /dev/null +++ b/recipes/_ts/basic/snippets/sources/static/images/Settings.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/recipes/_ts/basic/snippets/sources/static/index.html b/recipes/_ts/basic/snippets/sources/static/index.html new file mode 100644 index 0000000..ec8236f --- /dev/null +++ b/recipes/_ts/basic/snippets/sources/static/index.html @@ -0,0 +1,29 @@ + + + + + + + + + + + + + <%= htmlWebpackPlugin.options.APP_ROOT_ID %> + + + + +
    + + diff --git a/recipes/_ts/basic/snippets/sources/static/translations/en.json b/recipes/_ts/basic/snippets/sources/static/translations/en.json new file mode 100644 index 0000000..4aea446 --- /dev/null +++ b/recipes/_ts/basic/snippets/sources/static/translations/en.json @@ -0,0 +1,6 @@ +{ + "app_name": "Google Clone", + "user_name": "User name", + "dashboard": "Dashboard", + "other_module": "Other module name" +} diff --git a/recipes/_ts/basic/snippets/sources/utils/index.ts b/recipes/_ts/basic/snippets/sources/utils/index.ts new file mode 100644 index 0000000..40e15e0 --- /dev/null +++ b/recipes/_ts/basic/snippets/sources/utils/index.ts @@ -0,0 +1 @@ +export { default as RoutePaths } from './route-paths' diff --git a/recipes/_ts/basic/snippets/sources/utils/route-paths.tsx b/recipes/_ts/basic/snippets/sources/utils/route-paths.tsx new file mode 100644 index 0000000..6965dad --- /dev/null +++ b/recipes/_ts/basic/snippets/sources/utils/route-paths.tsx @@ -0,0 +1,5 @@ +export default { + SignIn: '/', + DashBoard: '/dashboard', + NotFound: '/not-found', +} diff --git a/recipes/_ts/basic/snippets/sources/utils/storage.tsx b/recipes/_ts/basic/snippets/sources/utils/storage.tsx new file mode 100644 index 0000000..92f25a2 --- /dev/null +++ b/recipes/_ts/basic/snippets/sources/utils/storage.tsx @@ -0,0 +1,18 @@ +const noop = () => '' + +export const storageAPI = (type = 'local') => { + if (typeof window !== 'undefined') { + if (type === 'local') { + return window.localStorage + } + + return window.sessionStorage + } + + return { + getItem: noop, + setItem: noop, + removeItem: noop, + clear: noop, + } +} \ No newline at end of file diff --git a/recipes/_ts/basic/snippets/sources/withI18n.js b/recipes/_ts/basic/snippets/sources/withI18n.js new file mode 100644 index 0000000..6ef40ba --- /dev/null +++ b/recipes/_ts/basic/snippets/sources/withI18n.js @@ -0,0 +1,40 @@ +function getSourceCode(appName, { sourceDir }) { +const langUrl = `${sourceDir.locales}/` + '${lang}.json' + +return `import React, { useState, useEffect, ComponentType, FC } from 'react' +import { IntlProvider } from 'react-intl' +import axios from 'axios' + +const fetchTranslations = async (lang: string) => { + return await axios.get>(\`${langUrl}\`) +} + +const withI18n =

    (Component: ComponentType

    ): FC

    => (props: P) => { + const [messages, setMessages] = useState>({}) + const locale = 'en' + + useEffect(() => { + fetchTranslations(locale).then((response) => { + setMessages(response.data) + }) + }, []) + + return ( + {}} + > + + + ) +} + +export default withI18n +` +} + +module.exports = { + getSourceCode, +}; \ No newline at end of file diff --git a/recipes/_ts/basic/snippets/tsconfig.json b/recipes/_ts/basic/snippets/tsconfig.json new file mode 100644 index 0000000..c74eb4b --- /dev/null +++ b/recipes/_ts/basic/snippets/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "baseUrl": "./src", + "paths": { + "@/ui/*": ["ui/*"], + "@components/*": ["components/*"], + "@/constants/*": ["constants/*"], + "@/hooks/*": ["hooks/*"], + "@/i18n/*": ["i18n/*"], + "@/modules/*": ["modules/*"], + "@/utils/*": ["utils/*"] + } + }, + "include": ["src"] +} diff --git a/recipes/_ts/basic/snippets/webpack.config.js b/recipes/_ts/basic/snippets/webpack.config.js new file mode 100644 index 0000000..bdbec2e --- /dev/null +++ b/recipes/_ts/basic/snippets/webpack.config.js @@ -0,0 +1,102 @@ +function getWebPackConfig(appName, { sourceDir, buildDir, portNumber }) { + return ` +const webpack = require("webpack"); +const path = require("path"); +const PACKAGE = require("./package.json"); + +// WebPack Plugins +const HtmlWebpackPlugin = require("html-webpack-plugin"); +const CopyPlugin = require("copy-webpack-plugin"); + +module.exports = { + entry: "./${sourceDir.main}/index.tsx", + + module: { + rules: [ + { + test: /\\.(ts|tsx)$/, + exclude: /node_modules/, + use: ["babel-loader"], + }, + { + test: /\\.js$/, + exclude: /node_modules/, + use: ["babel-loader"], + }, + { + test: /\\.svg$/, + use: ["@svgr/webpack", "file-loader"], + }, + { + test: /\\.(png|jpg|jpeg|gif)$/i, + use: [ + { + loader: "file-loader", + }, + ], + }, + ], + }, + + resolve: { + extensions: [".*", ".js", ".ts", ".tsx"], + alias: { + "@": path.resolve(__dirname, "src"), + }, + }, + + output: { + path: path.resolve(__dirname, "${buildDir}"), + filename: "${appName}.js", + chunkFilename: "[name].js", + }, + + plugins: [ + new webpack.HotModuleReplacementPlugin(), + new webpack.EnvironmentPlugin({ + VERSION: PACKAGE.version, + }), + new HtmlWebpackPlugin({ + inject: true, + template: path.resolve(__dirname, "${sourceDir.main}/${sourceDir.static}/index.html"), + APP_ROOT_ID: "${appName}", + APP_VERSION: PACKAGE.version, + }), + new CopyPlugin({ + patterns: [ + { + from: "./${sourceDir.main}/${sourceDir.static}/${sourceDir.images}", + to: "images", + }, + { + from: "./${sourceDir.main}/${sourceDir.static}/${sourceDir.locales}/en.json", + to: "${sourceDir.locales}/en.json", + }, + ], + }), + ], + + devServer: { + open: true, + historyApiFallback: true, + static: { + directory: path.resolve(__dirname, "${sourceDir.main}/${sourceDir.static}"), + }, + hot: true, + port: ${portNumber}, + proxy: [ + { + context: ["/api"], // list of paths to proxy + target: "http://YOUR_API_URL:9000", + changeOrigin: true, + secure: false, // optional: allows self-signed certificates + }, + ], + }, +}; +`; +} + +module.exports = { + getWebPackConfig, +}; diff --git a/recipes/install.js b/recipes/install.js index dd99256..a5b15bc 100644 --- a/recipes/install.js +++ b/recipes/install.js @@ -12,6 +12,10 @@ const slimSnippet = require("./slim/snippets"); const slimTypeScriptConfig = require("./_ts/slim/config"); const slimTypeScriptSnippet = require("./_ts/slim/snippets"); +// basic Typescript Version +const basicTypeScriptConfig = require("./_ts/basic/config"); +const basicTypeScriptSnippet = require("./_ts/basic/snippets"); + // Basic Javascript const basicConfig = require(`./basic/config`); const basicSnippet = require(`./basic/snippets`); @@ -31,7 +35,7 @@ const { getTwixtUIScripts } = require("./utils"); -const install = function(directory, appName = '') { +const install = function (directory, appName = '') { CFonts.say("React Chef", { type: 5, font: "block", // define the font face @@ -60,15 +64,17 @@ const install = function(directory, appName = '') { const defaultProjectType = 'slim'; const twixtUIProjectType = 'twixtui'; const slimTypescriptProjectType = 'slim typescript'; + const basicTypescriptProjectType = 'basic typescript'; let projectType = defaultProjectType; const isSlimProject = (type) => type === defaultProjectType; const isTwixtUIProject = (type) => type === twixtUIProjectType; const isSlimTypeScriptProject = (type) => type === slimTypescriptProjectType; - const isTypeScriptProject = (type) => isSlimTypeScriptProject(type); + const isBasicTypeScriptProject = (type) => type === basicTypescriptProjectType; + const isTypeScriptProject = (type) => isSlimTypeScriptProject(type) || isBasicTypeScriptProject(type); tryAccess(baseDirPath) .then(() => undefined, function onPathExist() { - if(!directory){ + if (!directory) { error( `Choose different App name. ${appName} is already exist in ${process.cwd()}` ); @@ -81,7 +87,7 @@ const install = function(directory, appName = '') { type: "list", name: "projectType", message: "choose your project type", - choices: ["Slim", "Slim TypeScript", "Basic", "TwixtUI"], + choices: ["Slim", "Slim TypeScript", "Basic", "Basic TypeScript", "TwixtUI",], default: "Slim", }, ]); @@ -89,7 +95,7 @@ const install = function(directory, appName = '') { .then((mainAnswer) => { projectType = mainAnswer.projectType.toLowerCase(); log(`projectType: ${projectType}`); - if(isSlimTypeScriptProject(projectType)){ + if (isSlimTypeScriptProject(projectType)) { log(`Slim Typescript - projectType: ${projectType}`); getConfig = slimTypeScriptConfig.getConfig; getModulesList = slimTypeScriptConfig.getModulesList; @@ -98,7 +104,16 @@ const install = function(directory, appName = '') { getWebPackConfig = slimTypeScriptSnippet.getWebPackConfig; getDynamicSourceCode = slimTypeScriptSnippet.getDynamicSourceCode; baseConfig = getConfig(); - } else if(isTwixtUIProject(projectType)){ + } else if (isBasicTypeScriptProject(projectType)) { + log(`Basic TypeScript - projectType: ${projectType}`); + getConfig = basicTypeScriptConfig.getConfig; + getModulesList = basicTypeScriptConfig.getModulesList; + getDevModulesList = basicTypeScriptConfig.getDevModulesList; + getFileContent = basicTypeScriptSnippet.getFileContent; + getWebPackConfig = basicTypeScriptSnippet.getWebPackConfig; + getDynamicSourceCode = basicTypeScriptSnippet.getDynamicSourceCode; + baseConfig = getConfig(); + } else if (isTwixtUIProject(projectType)) { log(`TwixtUI - projectType: ${projectType}`); getConfig = twixtUIConfig.getConfig; getModulesList = twixtUIConfig.getModulesList; @@ -132,7 +147,7 @@ const install = function(directory, appName = '') { }, ]; - if(baseConfig.canAdd.buildDir){ + if (baseConfig.canAdd.buildDir) { projectQuestions.push({ type: "input", name: "buildDir", @@ -181,7 +196,7 @@ const install = function(directory, appName = '') { return inquirer.prompt(projectQuestions); }) .then((answers) => { - if(!directory) { + if (!directory) { shell.mkdir(baseDirPath); shell.cd(appName); } else { @@ -193,6 +208,7 @@ const install = function(directory, appName = '') { }) .then((answers) => { const isTypeScriptProjectType = isTypeScriptProject(projectType); + const isBasicTypeScriptProjectType = isBasicTypeScriptProject(projectType); const fileExtension = isTypeScriptProjectType ? 'ts' : 'js'; const componentExtension = isTypeScriptProjectType ? 'tsx' : 'js'; if (baseConfig.canAdd.gitIgnore) { @@ -207,7 +223,7 @@ const install = function(directory, appName = '') { const tsConfigFileName = `tsconfig.json`; createFile(tsConfigFileName, getFileContent(tsConfigFileName)); } - + createFile( 'webpack.config.js', getWebPackConfig(appName, { @@ -233,20 +249,29 @@ const install = function(directory, appName = '') { shell.mkdir(baseConfig.sourceDir.main); shell.cd(baseConfig.sourceDir.main); + + let projectTypeName; + if (isBasicTypeScriptProjectType) { + projectTypeName = 'basic'; + } else if (isSlimTypeScriptProject(projectType)) { + projectTypeName = 'slim'; + } else { + projectTypeName = projectType; + } + const sourceSubBase = isTypeScriptProjectType ? '_ts/' : ''; - const projectTypeName = isSlimTypeScriptProject(projectType) ? 'slim' : projectType; const sourceSnippetDir = `${__dirname}/${sourceSubBase}${projectTypeName}/snippets/sources`; const indexSourceFileName = `index.js`; - const indexFileNameToCreateWithPath = !isTwixtUIProject(projectType) ? `index.${componentExtension}`: getTwixtUIIndexPath(projectType); + const indexFileNameToCreateWithPath = !isTwixtUIProject(projectType) ? `index.${componentExtension}` : getTwixtUIIndexPath(projectType); createFile(indexFileNameToCreateWithPath, getDynamicSourceCode(indexSourceFileName, appName, baseConfig)); const appSourceFileName = `App.js`; - const appFileNameToCreateWithPath = !isTwixtUIProject(projectType)? `App.${componentExtension}` : getTwixtUIHomePath(projectType); + const appFileNameToCreateWithPath = !isTwixtUIProject(projectType) ? `App.${componentExtension}` : getTwixtUIHomePath(projectType); createFile(appFileNameToCreateWithPath, getDynamicSourceCode(appSourceFileName, appName, baseConfig)); if (baseConfig.canAdd.routes) { - const RoutesFile = "Routes.js"; + const RoutesFile = `Routes.${componentExtension}`; createFile( RoutesFile, getDynamicSourceCode(RoutesFile, appName, baseConfig) @@ -289,7 +314,7 @@ const install = function(directory, appName = '') { shell.cp("-Rf", `${sourceSnippetDir}/i18n`, "."); shell.cd(baseConfig.sourceDir.i18n); - const withI18n = `withI18n.js`; + const withI18n = `withI18n.${componentExtension}`; createFile(withI18n, getDynamicSourceCode(withI18n, appName, baseConfig)); shell.cd(".."); } @@ -301,14 +326,14 @@ const install = function(directory, appName = '') { shell.cd( `${baseConfig.sourceDir.containers}/${baseConfig.modules.signIn}` ); - const signInModule = "SignIn.js"; + const signInModule = `SignIn.${componentExtension}`; createFile( signInModule, getDynamicSourceCode(signInModule, appName, baseConfig) ); shell.cd(`../${baseConfig.modules.dashboard}`); - const dashboardModule = "Dashboard.js"; + const dashboardModule = `Dashboard.${componentExtension}`; createFile( dashboardModule, getDynamicSourceCode(dashboardModule, appName, baseConfig) @@ -326,7 +351,7 @@ const install = function(directory, appName = '') { shell.cd( `${baseConfig.sourceDir.components}/${baseConfig.sourceDir.businessLogic}/Loader/${pageLoader}` ); - const pageLoaderBlock = `${pageLoader}.js`; + const pageLoaderBlock = `${pageLoader}.${componentExtension}`; createFile( pageLoaderBlock, getDynamicSourceCode(pageLoaderBlock, appName, baseConfig) @@ -334,7 +359,7 @@ const install = function(directory, appName = '') { const sidebar = "Sidebar"; shell.cd(`../../Region/${sidebar}`); - const sidebarBlock = `${sidebar}.js`; + const sidebarBlock = `${sidebar}.${componentExtension}`; createFile( sidebarBlock, getDynamicSourceCode(sidebarBlock, appName, baseConfig) @@ -342,7 +367,7 @@ const install = function(directory, appName = '') { const topBar = "TopBar"; shell.cd(`../../Region/${topBar}`); - const topBarBlock = `${topBar}.js`; + const topBarBlock = `${topBar}.${componentExtension}`; createFile( topBarBlock, getDynamicSourceCode(topBarBlock, appName, baseConfig) @@ -376,16 +401,16 @@ const install = function(directory, appName = '') { build: "webpack --mode production --progress", ...(answers.eslint ? { - lint: "eslint src --ext .js", - } + lint: "eslint src --ext .js", + } : {}), ...(answers.prettier ? { - prettier: "prettier --write src", - } + prettier: "prettier --write src", + } : {}), clean: "rm -rf node_modules", - ...(isTwixtUIProject(projectType) ? getTwixtUIScripts(): {}) + ...(isTwixtUIProject(projectType) ? getTwixtUIScripts() : {}) }; delete packageFileObject.main; shell.rm("package.json"); diff --git a/recipes/moduleMatrix.js b/recipes/moduleMatrix.js index 87473eb..6b7bcfe 100644 --- a/recipes/moduleMatrix.js +++ b/recipes/moduleMatrix.js @@ -24,6 +24,7 @@ module.exports = { babel, slimDev: [...webpack, ...webpackPlugins, ...webpackLoaders, ...babel], slimTypescriptDev: [...typeScriptTooling, ...webpack, ...webpackPlugins, ...webpackLoaders, ...babelWithTypeScript], + basicTypescriptDev: [...typeScriptTooling, ...webpack, ...webpackPlugins, ...webpackLoaders, ...babelWithTypeScript], twixtUIDev: [...webpack, ...webpackPlugins, ...webpackLoaders, ...wepPackStyleLoaders, ...babel], husky: 'npm i -D husky', eslint: 'npx install-peerdeps --dev eslint-config-airbnb',