├── .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 |
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 |
--------------------------------------------------------------------------------