├── .eslintrc.cjs ├── .gitignore ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── public └── vite.svg ├── src ├── assets │ └── react.svg ├── components │ └── sidebar.tsx ├── context │ └── provider.tsx ├── layouts │ ├── fullscreen-layout.tsx │ └── sidebar-layout.tsx ├── main.tsx ├── routeTree.gen.ts ├── routes │ ├── __root.tsx │ ├── _app.tsx │ ├── _app │ │ └── $workspace │ │ │ └── index.tsx │ ├── _onboarding.tsx │ ├── _onboarding │ │ └── getting-started.tsx │ └── index.tsx └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | { allowConstantExport: true }, 16 | ], 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: 13 | 14 | - Configure the top-level `parserOptions` property like this: 15 | 16 | ```js 17 | export default { 18 | // other rules... 19 | parserOptions: { 20 | ecmaVersion: 'latest', 21 | sourceType: 'module', 22 | project: ['./tsconfig.json', './tsconfig.node.json'], 23 | tsconfigRootDir: __dirname, 24 | }, 25 | } 26 | ``` 27 | 28 | - Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` 29 | - Optionally add `plugin:@typescript-eslint/stylistic-type-checked` 30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list 31 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-saas", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "imports": { 13 | "#*": [ 14 | "./src/*", 15 | "./src/*.ts", 16 | "./src/*.tsx" 17 | ] 18 | }, 19 | "dependencies": { 20 | "@chakra-ui/next-js": "^2.2.0", 21 | "@chakra-ui/react": "^2.8.2", 22 | "@emotion/react": "^11.11.3", 23 | "@emotion/styled": "^11.11.0", 24 | "@saas-ui/react": "^2.6.0", 25 | "@tanstack/react-query": "^5.22.2", 26 | "@tanstack/react-query-devtools": "^5.24.0", 27 | "@tanstack/react-router": "^1.16.6", 28 | "@tanstack/router-devtools": "^1.16.6", 29 | "@tanstack/router-vite-plugin": "^1.16.5", 30 | "framer-motion": "^11.0.5", 31 | "react": "^18.2.0", 32 | "react-dom": "^18.2.0" 33 | }, 34 | "devDependencies": { 35 | "@types/react": "^18.2.56", 36 | "@types/react-dom": "^18.2.19", 37 | "@typescript-eslint/eslint-plugin": "^7.0.2", 38 | "@typescript-eslint/parser": "^7.0.2", 39 | "@vitejs/plugin-react": "^4.2.1", 40 | "eslint": "^8.56.0", 41 | "eslint-plugin-react-hooks": "^4.6.0", 42 | "eslint-plugin-react-refresh": "^0.4.5", 43 | "typescript": "^5.2.2", 44 | "vite": "^5.1.4" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/sidebar.tsx: -------------------------------------------------------------------------------- 1 | import { NavGroup, NavItem, Sidebar, SidebarSection } from "@saas-ui/react"; 2 | import { getRouteApi } from "@tanstack/react-router"; 3 | 4 | const route = getRouteApi("/_app/$workspace/"); 5 | 6 | export const AppSidebar = () => { 7 | const params = route.useParams(); 8 | 9 | return ( 10 | 11 | 12 | 13 | Home 14 | 15 | 16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /src/context/provider.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef } from "react"; 2 | import { LinkProps, SaasProvider } from "@saas-ui/react"; 3 | import { RouterProvider, createRouter, Link } from "@tanstack/react-router"; 4 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 5 | 6 | import { routeTree } from "../routeTree.gen"; 7 | 8 | const queryClient = new QueryClient(); 9 | 10 | // Set up a Router instance 11 | const router = createRouter({ 12 | routeTree, 13 | context: { 14 | queryClient, 15 | }, 16 | defaultPreload: "intent", 17 | // Since we're using React Query, we don't want loader calls to ever be stale 18 | // This will ensure that the loader is always called when the route is preloaded or visited 19 | defaultPreloadStaleTime: 0, 20 | }); 21 | 22 | // Register things for typesafety 23 | declare module "@tanstack/react-router" { 24 | interface Register { 25 | router: typeof router; 26 | } 27 | } 28 | 29 | // This makes sure Saas UI components use our router 30 | const LinkComponent = forwardRef>( 31 | (props, ref) => { 32 | const { href, ...rest } = props; 33 | return ; 34 | } 35 | ); 36 | 37 | export const Provider = () => { 38 | return ( 39 | 40 | 41 | 42 | 43 | 44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /src/layouts/fullscreen-layout.tsx: -------------------------------------------------------------------------------- 1 | import { AppShell } from "@saas-ui/react"; 2 | 3 | export const FullscreenLayout: React.FC = (props) => { 4 | return {props.children}; 5 | }; 6 | -------------------------------------------------------------------------------- /src/layouts/sidebar-layout.tsx: -------------------------------------------------------------------------------- 1 | import { AppShell } from "@saas-ui/react"; 2 | 3 | import { AppSidebar } from "#components/sidebar"; 4 | 5 | export const SidebarLayout: React.FC = (props) => { 6 | return ( 7 | }> 8 | {props.children} 9 | 10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import { Provider } from "./context/provider.tsx"; 4 | 5 | ReactDOM.createRoot(document.getElementById("root")!).render( 6 | 7 | 8 | 9 | ); 10 | -------------------------------------------------------------------------------- /src/routeTree.gen.ts: -------------------------------------------------------------------------------- 1 | /* prettier-ignore-start */ 2 | 3 | /* eslint-disable */ 4 | 5 | // @ts-nocheck 6 | 7 | // noinspection JSUnusedGlobalSymbols 8 | 9 | // This file is auto-generated by TanStack Router 10 | 11 | // Import Routes 12 | 13 | import { Route as rootRoute } from './routes/__root' 14 | import { Route as OnboardingImport } from './routes/_onboarding' 15 | import { Route as AppImport } from './routes/_app' 16 | import { Route as IndexImport } from './routes/index' 17 | import { Route as OnboardingGettingStartedImport } from './routes/_onboarding/getting-started' 18 | import { Route as AppWorkspaceIndexImport } from './routes/_app/$workspace/index' 19 | 20 | // Create/Update Routes 21 | 22 | const OnboardingRoute = OnboardingImport.update({ 23 | id: '/_onboarding', 24 | getParentRoute: () => rootRoute, 25 | } as any) 26 | 27 | const AppRoute = AppImport.update({ 28 | id: '/_app', 29 | getParentRoute: () => rootRoute, 30 | } as any) 31 | 32 | const IndexRoute = IndexImport.update({ 33 | path: '/', 34 | getParentRoute: () => rootRoute, 35 | } as any) 36 | 37 | const OnboardingGettingStartedRoute = OnboardingGettingStartedImport.update({ 38 | path: '/getting-started', 39 | getParentRoute: () => OnboardingRoute, 40 | } as any) 41 | 42 | const AppWorkspaceIndexRoute = AppWorkspaceIndexImport.update({ 43 | path: '/$workspace/', 44 | getParentRoute: () => AppRoute, 45 | } as any) 46 | 47 | // Populate the FileRoutesByPath interface 48 | 49 | declare module '@tanstack/react-router' { 50 | interface FileRoutesByPath { 51 | '/': { 52 | preLoaderRoute: typeof IndexImport 53 | parentRoute: typeof rootRoute 54 | } 55 | '/_app': { 56 | preLoaderRoute: typeof AppImport 57 | parentRoute: typeof rootRoute 58 | } 59 | '/_onboarding': { 60 | preLoaderRoute: typeof OnboardingImport 61 | parentRoute: typeof rootRoute 62 | } 63 | '/_onboarding/getting-started': { 64 | preLoaderRoute: typeof OnboardingGettingStartedImport 65 | parentRoute: typeof OnboardingImport 66 | } 67 | '/_app/$workspace/': { 68 | preLoaderRoute: typeof AppWorkspaceIndexImport 69 | parentRoute: typeof AppImport 70 | } 71 | } 72 | } 73 | 74 | // Create and export the route tree 75 | 76 | export const routeTree = rootRoute.addChildren([ 77 | IndexRoute, 78 | AppRoute.addChildren([AppWorkspaceIndexRoute]), 79 | OnboardingRoute.addChildren([OnboardingGettingStartedRoute]), 80 | ]) 81 | 82 | /* prettier-ignore-end */ 83 | -------------------------------------------------------------------------------- /src/routes/__root.tsx: -------------------------------------------------------------------------------- 1 | import { QueryClient } from "@tanstack/react-query"; 2 | import { createRootRouteWithContext, Outlet } from "@tanstack/react-router"; 3 | import { TanStackRouterDevtools } from "@tanstack/router-devtools"; 4 | 5 | export const Route = createRootRouteWithContext<{ 6 | queryClient: QueryClient; 7 | }>()({ 8 | component: () => ( 9 | <> 10 | 11 | 12 | 13 | ), 14 | }); 15 | -------------------------------------------------------------------------------- /src/routes/_app.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet, createFileRoute } from "@tanstack/react-router"; 2 | 3 | import { SidebarLayout } from "#layouts/sidebar-layout"; 4 | 5 | export const Route = createFileRoute("/_app")({ 6 | component: AppLayout, 7 | }); 8 | 9 | function AppLayout() { 10 | return ( 11 | 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/routes/_app/$workspace/index.tsx: -------------------------------------------------------------------------------- 1 | import { Center } from "@chakra-ui/react"; 2 | import { EmptyState } from "@saas-ui/react"; 3 | import { createFileRoute } from "@tanstack/react-router"; 4 | 5 | export const Route = createFileRoute("/_app/$workspace/")({ 6 | component: Home, 7 | }); 8 | 9 | function Home() { 10 | return ( 11 |
12 | 17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/routes/_onboarding.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet, createFileRoute } from "@tanstack/react-router"; 2 | 3 | import { FullscreenLayout } from "#layouts/fullscreen-layout"; 4 | 5 | export const Route = createFileRoute("/_onboarding")({ 6 | component: OnboardingLayout, 7 | }); 8 | 9 | function OnboardingLayout() { 10 | return ( 11 | 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/routes/_onboarding/getting-started.tsx: -------------------------------------------------------------------------------- 1 | import { Center, Container, Heading } from "@chakra-ui/react"; 2 | import { Field, Form, FormLayout, SubmitButton } from "@saas-ui/react"; 3 | import { useMutation } from "@tanstack/react-query"; 4 | import { createFileRoute, useNavigate } from "@tanstack/react-router"; 5 | import { FormEvent } from "react"; 6 | 7 | const slugify = (value: string) => { 8 | return value 9 | .trim() 10 | .toLocaleLowerCase() 11 | .replace(/[^\w\s-]/g, "") 12 | .replace(/[\s_-]+/g, "-") 13 | .replace(/^-+|-+$/g, ""); 14 | }; 15 | 16 | export const Route = createFileRoute("/_onboarding/getting-started")({ 17 | component: GettingStarted, 18 | }); 19 | 20 | interface OnboardingData { 21 | organization: string; 22 | workspace: string; 23 | } 24 | 25 | function GettingStarted() { 26 | const navigate = useNavigate(); 27 | 28 | const submit = useMutation({ 29 | mutationFn: async (values) => { 30 | localStorage.setItem("workspace", values.workspace); 31 | }, 32 | onSuccess: (data, variables) => { 33 | navigate({ 34 | to: "/$workspace", 35 | params: { workspace: variables.workspace }, 36 | }); 37 | }, 38 | }); 39 | 40 | return ( 41 |
42 | 43 | 44 | Getting started 45 | 46 |
submit.mutateAsync(data)} 48 | defaultValues={{ 49 | organization: "", 50 | workspace: "", 51 | }} 52 | > 53 | {({ setValue }) => ( 54 | 55 | ) => { 59 | const value = e.currentTarget.value; 60 | setValue("organization", value); 61 | setValue("workspace", slugify(value)); 62 | }} 63 | /> 64 | 65 | Continue 66 | 67 | )} 68 |
69 |
70 |
71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /src/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Heading } from "@chakra-ui/react"; 2 | import { createFileRoute, redirect } from "@tanstack/react-router"; 3 | 4 | export const Route = createFileRoute("/")({ 5 | component: Index, 6 | beforeLoad: async () => { 7 | // We will add authentication here later 8 | const user = { 9 | id: "123", 10 | email: "john.doe@acme.com", 11 | }; 12 | 13 | if (!user) { 14 | return; 15 | } 16 | 17 | const workspace = localStorage.getItem("workspace"); 18 | 19 | const path = workspace ? `/${workspace}` : "/getting-started"; 20 | 21 | throw redirect({ 22 | to: path, 23 | }); 24 | }, 25 | }); 26 | 27 | function Index() { 28 | return ( 29 | 30 | 31 | Welcome Home! 32 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true 9 | }, 10 | "include": ["vite.config.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | import { TanStackRouterVite } from "@tanstack/router-vite-plugin"; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react(), TanStackRouterVite()], 8 | }); 9 | --------------------------------------------------------------------------------