init commit
This commit is contained in:
		
							parent
							
								
									f7bc4ab449
								
							
						
					
					
						commit
						d8c40fb093
					
				
							
								
								
									
										11
									
								
								.cta.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								.cta.json
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,11 @@
 | 
			
		|||
{
 | 
			
		||||
  "projectName": ".",
 | 
			
		||||
  "mode": "file-router",
 | 
			
		||||
  "typescript": true,
 | 
			
		||||
  "tailwind": false,
 | 
			
		||||
  "packageManager": "npm",
 | 
			
		||||
  "git": true,
 | 
			
		||||
  "version": 1,
 | 
			
		||||
  "framework": "react-cra",
 | 
			
		||||
  "chosenAddOns": []
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										140
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										140
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -1,132 +1,10 @@
 | 
			
		|||
# ---> Node
 | 
			
		||||
# Logs
 | 
			
		||||
logs
 | 
			
		||||
*.log
 | 
			
		||||
npm-debug.log*
 | 
			
		||||
yarn-debug.log*
 | 
			
		||||
yarn-error.log*
 | 
			
		||||
lerna-debug.log*
 | 
			
		||||
.pnpm-debug.log*
 | 
			
		||||
 | 
			
		||||
# Diagnostic reports (https://nodejs.org/api/report.html)
 | 
			
		||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
 | 
			
		||||
 | 
			
		||||
# Runtime data
 | 
			
		||||
pids
 | 
			
		||||
*.pid
 | 
			
		||||
*.seed
 | 
			
		||||
*.pid.lock
 | 
			
		||||
 | 
			
		||||
# Directory for instrumented libs generated by jscoverage/JSCover
 | 
			
		||||
lib-cov
 | 
			
		||||
 | 
			
		||||
# Coverage directory used by tools like istanbul
 | 
			
		||||
coverage
 | 
			
		||||
*.lcov
 | 
			
		||||
 | 
			
		||||
# nyc test coverage
 | 
			
		||||
.nyc_output
 | 
			
		||||
 | 
			
		||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
 | 
			
		||||
.grunt
 | 
			
		||||
 | 
			
		||||
# Bower dependency directory (https://bower.io/)
 | 
			
		||||
bower_components
 | 
			
		||||
 | 
			
		||||
# node-waf configuration
 | 
			
		||||
.lock-wscript
 | 
			
		||||
 | 
			
		||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
 | 
			
		||||
build/Release
 | 
			
		||||
 | 
			
		||||
# Dependency directories
 | 
			
		||||
node_modules/
 | 
			
		||||
jspm_packages/
 | 
			
		||||
 | 
			
		||||
# Snowpack dependency directory (https://snowpack.dev/)
 | 
			
		||||
web_modules/
 | 
			
		||||
 | 
			
		||||
# TypeScript cache
 | 
			
		||||
*.tsbuildinfo
 | 
			
		||||
 | 
			
		||||
# Optional npm cache directory
 | 
			
		||||
.npm
 | 
			
		||||
 | 
			
		||||
# Optional eslint cache
 | 
			
		||||
.eslintcache
 | 
			
		||||
 | 
			
		||||
# Optional stylelint cache
 | 
			
		||||
.stylelintcache
 | 
			
		||||
 | 
			
		||||
# Microbundle cache
 | 
			
		||||
.rpt2_cache/
 | 
			
		||||
.rts2_cache_cjs/
 | 
			
		||||
.rts2_cache_es/
 | 
			
		||||
.rts2_cache_umd/
 | 
			
		||||
 | 
			
		||||
# Optional REPL history
 | 
			
		||||
.node_repl_history
 | 
			
		||||
 | 
			
		||||
# Output of 'npm pack'
 | 
			
		||||
*.tgz
 | 
			
		||||
 | 
			
		||||
# Yarn Integrity file
 | 
			
		||||
.yarn-integrity
 | 
			
		||||
 | 
			
		||||
# dotenv environment variable files
 | 
			
		||||
.env
 | 
			
		||||
.env.development.local
 | 
			
		||||
.env.test.local
 | 
			
		||||
.env.production.local
 | 
			
		||||
.env.local
 | 
			
		||||
 | 
			
		||||
# parcel-bundler cache (https://parceljs.org/)
 | 
			
		||||
.cache
 | 
			
		||||
.parcel-cache
 | 
			
		||||
 | 
			
		||||
# Next.js build output
 | 
			
		||||
.next
 | 
			
		||||
out
 | 
			
		||||
 | 
			
		||||
# Nuxt.js build / generate output
 | 
			
		||||
.nuxt
 | 
			
		||||
node_modules
 | 
			
		||||
.DS_Store
 | 
			
		||||
dist
 | 
			
		||||
 | 
			
		||||
# Gatsby files
 | 
			
		||||
.cache/
 | 
			
		||||
# Comment in the public line in if your project uses Gatsby and not Next.js
 | 
			
		||||
# https://nextjs.org/blog/next-9-1#public-directory-support
 | 
			
		||||
# public
 | 
			
		||||
 | 
			
		||||
# vuepress build output
 | 
			
		||||
.vuepress/dist
 | 
			
		||||
 | 
			
		||||
# vuepress v2.x temp and cache directory
 | 
			
		||||
.temp
 | 
			
		||||
.cache
 | 
			
		||||
 | 
			
		||||
# Docusaurus cache and generated files
 | 
			
		||||
.docusaurus
 | 
			
		||||
 | 
			
		||||
# Serverless directories
 | 
			
		||||
.serverless/
 | 
			
		||||
 | 
			
		||||
# FuseBox cache
 | 
			
		||||
.fusebox/
 | 
			
		||||
 | 
			
		||||
# DynamoDB Local files
 | 
			
		||||
.dynamodb/
 | 
			
		||||
 | 
			
		||||
# TernJS port file
 | 
			
		||||
.tern-port
 | 
			
		||||
 | 
			
		||||
# Stores VSCode versions used for testing VSCode extensions
 | 
			
		||||
.vscode-test
 | 
			
		||||
 | 
			
		||||
# yarn v2
 | 
			
		||||
.yarn/cache
 | 
			
		||||
.yarn/unplugged
 | 
			
		||||
.yarn/build-state.yml
 | 
			
		||||
.yarn/install-state.gz
 | 
			
		||||
.pnp.*
 | 
			
		||||
 | 
			
		||||
dist-ssr
 | 
			
		||||
*.local
 | 
			
		||||
count.txt
 | 
			
		||||
.env
 | 
			
		||||
.nitro
 | 
			
		||||
.tanstack
 | 
			
		||||
.vscode/
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										290
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										290
									
								
								README.md
									
									
									
									
									
								
							| 
						 | 
				
			
			@ -1,2 +1,290 @@
 | 
			
		|||
# TTMT.WebGUI
 | 
			
		||||
Welcome to your new TanStack app! 
 | 
			
		||||
 | 
			
		||||
# Getting Started
 | 
			
		||||
 | 
			
		||||
To run this application:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
npm install
 | 
			
		||||
npm run start  
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
# Building For Production
 | 
			
		||||
 | 
			
		||||
To build this application for production:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
npm run build
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Testing
 | 
			
		||||
 | 
			
		||||
This project uses [Vitest](https://vitest.dev/) for testing. You can run the tests with:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
npm run test
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Styling
 | 
			
		||||
 | 
			
		||||
This project uses CSS for styling.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## Routing
 | 
			
		||||
This project uses [TanStack Router](https://tanstack.com/router). The initial setup is a file based router. Which means that the routes are managed as files in `src/routes`.
 | 
			
		||||
 | 
			
		||||
### Adding A Route
 | 
			
		||||
 | 
			
		||||
To add a new route to your application just add another a new file in the `./src/routes` directory.
 | 
			
		||||
 | 
			
		||||
TanStack will automatically generate the content of the route file for you.
 | 
			
		||||
 | 
			
		||||
Now that you have two routes you can use a `Link` component to navigate between them.
 | 
			
		||||
 | 
			
		||||
### Adding Links
 | 
			
		||||
 | 
			
		||||
To use SPA (Single Page Application) navigation you will need to import the `Link` component from `@tanstack/react-router`.
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
import { Link } from "@tanstack/react-router";
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Then anywhere in your JSX you can use it like so:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
<Link to="/about">About</Link>
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
This will create a link that will navigate to the `/about` route.
 | 
			
		||||
 | 
			
		||||
More information on the `Link` component can be found in the [Link documentation](https://tanstack.com/router/v1/docs/framework/react/api/router/linkComponent).
 | 
			
		||||
 | 
			
		||||
### Using A Layout
 | 
			
		||||
 | 
			
		||||
In the File Based Routing setup the layout is located in `src/routes/__root.tsx`. Anything you add to the root route will appear in all the routes. The route content will appear in the JSX where you use the `<Outlet />` component.
 | 
			
		||||
 | 
			
		||||
Here is an example layout that includes a header:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
import { Outlet, createRootRoute } from '@tanstack/react-router'
 | 
			
		||||
import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'
 | 
			
		||||
 | 
			
		||||
import { Link } from "@tanstack/react-router";
 | 
			
		||||
 | 
			
		||||
export const Route = createRootRoute({
 | 
			
		||||
  component: () => (
 | 
			
		||||
    <>
 | 
			
		||||
      <header>
 | 
			
		||||
        <nav>
 | 
			
		||||
          <Link to="/">Home</Link>
 | 
			
		||||
          <Link to="/about">About</Link>
 | 
			
		||||
        </nav>
 | 
			
		||||
      </header>
 | 
			
		||||
      <Outlet />
 | 
			
		||||
      <TanStackRouterDevtools />
 | 
			
		||||
    </>
 | 
			
		||||
  ),
 | 
			
		||||
})
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
The `<TanStackRouterDevtools />` component is not required so you can remove it if you don't want it in your layout.
 | 
			
		||||
 | 
			
		||||
More information on layouts can be found in the [Layouts documentation](https://tanstack.com/router/latest/docs/framework/react/guide/routing-concepts#layouts).
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## Data Fetching
 | 
			
		||||
 | 
			
		||||
There are multiple ways to fetch data in your application. You can use TanStack Query to fetch data from a server. But you can also use the `loader` functionality built into TanStack Router to load the data for a route before it's rendered.
 | 
			
		||||
 | 
			
		||||
For example:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
const peopleRoute = createRoute({
 | 
			
		||||
  getParentRoute: () => rootRoute,
 | 
			
		||||
  path: "/people",
 | 
			
		||||
  loader: async () => {
 | 
			
		||||
    const response = await fetch("https://swapi.dev/api/people");
 | 
			
		||||
    return response.json() as Promise<{
 | 
			
		||||
      results: {
 | 
			
		||||
        name: string;
 | 
			
		||||
      }[];
 | 
			
		||||
    }>;
 | 
			
		||||
  },
 | 
			
		||||
  component: () => {
 | 
			
		||||
    const data = peopleRoute.useLoaderData();
 | 
			
		||||
    return (
 | 
			
		||||
      <ul>
 | 
			
		||||
        {data.results.map((person) => (
 | 
			
		||||
          <li key={person.name}>{person.name}</li>
 | 
			
		||||
        ))}
 | 
			
		||||
      </ul>
 | 
			
		||||
    );
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Loaders simplify your data fetching logic dramatically. Check out more information in the [Loader documentation](https://tanstack.com/router/latest/docs/framework/react/guide/data-loading#loader-parameters).
 | 
			
		||||
 | 
			
		||||
### React-Query
 | 
			
		||||
 | 
			
		||||
React-Query is an excellent addition or alternative to route loading and integrating it into you application is a breeze.
 | 
			
		||||
 | 
			
		||||
First add your dependencies:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
npm install @tanstack/react-query @tanstack/react-query-devtools
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Next we'll need to create a query client and provider. We recommend putting those in `main.tsx`.
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
 | 
			
		||||
 | 
			
		||||
// ...
 | 
			
		||||
 | 
			
		||||
const queryClient = new QueryClient();
 | 
			
		||||
 | 
			
		||||
// ...
 | 
			
		||||
 | 
			
		||||
if (!rootElement.innerHTML) {
 | 
			
		||||
  const root = ReactDOM.createRoot(rootElement);
 | 
			
		||||
 | 
			
		||||
  root.render(
 | 
			
		||||
    <QueryClientProvider client={queryClient}>
 | 
			
		||||
      <RouterProvider router={router} />
 | 
			
		||||
    </QueryClientProvider>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
You can also add TanStack Query Devtools to the root route (optional).
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
 | 
			
		||||
 | 
			
		||||
const rootRoute = createRootRoute({
 | 
			
		||||
  component: () => (
 | 
			
		||||
    <>
 | 
			
		||||
      <Outlet />
 | 
			
		||||
      <ReactQueryDevtools buttonPosition="top-right" />
 | 
			
		||||
      <TanStackRouterDevtools />
 | 
			
		||||
    </>
 | 
			
		||||
  ),
 | 
			
		||||
});
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Now you can use `useQuery` to fetch your data.
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
import { useQuery } from "@tanstack/react-query";
 | 
			
		||||
 | 
			
		||||
import "./App.css";
 | 
			
		||||
 | 
			
		||||
function App() {
 | 
			
		||||
  const { data } = useQuery({
 | 
			
		||||
    queryKey: ["people"],
 | 
			
		||||
    queryFn: () =>
 | 
			
		||||
      fetch("https://swapi.dev/api/people")
 | 
			
		||||
        .then((res) => res.json())
 | 
			
		||||
        .then((data) => data.results as { name: string }[]),
 | 
			
		||||
    initialData: [],
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      <ul>
 | 
			
		||||
        {data.map((person) => (
 | 
			
		||||
          <li key={person.name}>{person.name}</li>
 | 
			
		||||
        ))}
 | 
			
		||||
      </ul>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default App;
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
You can find out everything you need to know on how to use React-Query in the [React-Query documentation](https://tanstack.com/query/latest/docs/framework/react/overview).
 | 
			
		||||
 | 
			
		||||
## State Management
 | 
			
		||||
 | 
			
		||||
Another common requirement for React applications is state management. There are many options for state management in React. TanStack Store provides a great starting point for your project.
 | 
			
		||||
 | 
			
		||||
First you need to add TanStack Store as a dependency:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
npm install @tanstack/store
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Now let's create a simple counter in the `src/App.tsx` file as a demonstration.
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
import { useStore } from "@tanstack/react-store";
 | 
			
		||||
import { Store } from "@tanstack/store";
 | 
			
		||||
import "./App.css";
 | 
			
		||||
 | 
			
		||||
const countStore = new Store(0);
 | 
			
		||||
 | 
			
		||||
function App() {
 | 
			
		||||
  const count = useStore(countStore);
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      <button onClick={() => countStore.setState((n) => n + 1)}>
 | 
			
		||||
        Increment - {count}
 | 
			
		||||
      </button>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default App;
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
One of the many nice features of TanStack Store is the ability to derive state from other state. That derived state will update when the base state updates.
 | 
			
		||||
 | 
			
		||||
Let's check this out by doubling the count using derived state.
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
import { useStore } from "@tanstack/react-store";
 | 
			
		||||
import { Store, Derived } from "@tanstack/store";
 | 
			
		||||
import "./App.css";
 | 
			
		||||
 | 
			
		||||
const countStore = new Store(0);
 | 
			
		||||
 | 
			
		||||
const doubledStore = new Derived({
 | 
			
		||||
  fn: () => countStore.state * 2,
 | 
			
		||||
  deps: [countStore],
 | 
			
		||||
});
 | 
			
		||||
doubledStore.mount();
 | 
			
		||||
 | 
			
		||||
function App() {
 | 
			
		||||
  const count = useStore(countStore);
 | 
			
		||||
  const doubledCount = useStore(doubledStore);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      <button onClick={() => countStore.setState((n) => n + 1)}>
 | 
			
		||||
        Increment - {count}
 | 
			
		||||
      </button>
 | 
			
		||||
      <div>Doubled - {doubledCount}</div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default App;
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
We use the `Derived` class to create a new store that is derived from another store. The `Derived` class has a `mount` method that will start the derived store updating.
 | 
			
		||||
 | 
			
		||||
Once we've created the derived store we can use it in the `App` component just like we would any other store using the `useStore` hook.
 | 
			
		||||
 | 
			
		||||
You can find out everything you need to know on how to use TanStack Store in the [TanStack Store documentation](https://tanstack.com/store/latest).
 | 
			
		||||
 | 
			
		||||
# Demo files
 | 
			
		||||
 | 
			
		||||
Files prefixed with `demo` can be safely deleted. They are there to provide a starting point for you to play around with the features you've installed.
 | 
			
		||||
 | 
			
		||||
# Learn More
 | 
			
		||||
 | 
			
		||||
You can learn more about all of the offerings from TanStack in the [TanStack documentation](https://tanstack.com).
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										21
									
								
								components.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								components.json
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,21 @@
 | 
			
		|||
{
 | 
			
		||||
  "$schema": "https://ui.shadcn.com/schema.json",
 | 
			
		||||
  "style": "new-york",
 | 
			
		||||
  "rsc": false,
 | 
			
		||||
  "tsx": true,
 | 
			
		||||
  "tailwind": {
 | 
			
		||||
    "config": "",
 | 
			
		||||
    "css": "src/styles.css",
 | 
			
		||||
    "baseColor": "gray",
 | 
			
		||||
    "cssVariables": true,
 | 
			
		||||
    "prefix": ""
 | 
			
		||||
  },
 | 
			
		||||
  "aliases": {
 | 
			
		||||
    "components": "@/components",
 | 
			
		||||
    "utils": "@/lib/utils",
 | 
			
		||||
    "ui": "@/components/ui",
 | 
			
		||||
    "lib": "@/lib",
 | 
			
		||||
    "hooks": "@/hooks"
 | 
			
		||||
  },
 | 
			
		||||
  "iconLibrary": "lucide"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										20
									
								
								index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								index.html
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,20 @@
 | 
			
		|||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
  <head>
 | 
			
		||||
    <meta charset="UTF-8" />
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 | 
			
		||||
    <link rel="icon" href="/favicon.ico" />
 | 
			
		||||
    <meta name="theme-color" content="#000000" />
 | 
			
		||||
    <meta
 | 
			
		||||
      name="description"
 | 
			
		||||
      content="Web site created using create-tsrouter-app"
 | 
			
		||||
    />
 | 
			
		||||
    <link rel="apple-touch-icon" href="/logo192.png" />
 | 
			
		||||
    <link rel="manifest" href="/manifest.json" />
 | 
			
		||||
    <title>Create TanStack App - .</title>
 | 
			
		||||
  </head>
 | 
			
		||||
  <body>
 | 
			
		||||
    <div id="app"></div>
 | 
			
		||||
    <script type="module" src="/src/main.tsx"></script>
 | 
			
		||||
  </body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										7676
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										7676
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										52
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,52 @@
 | 
			
		|||
{
 | 
			
		||||
  "name": ".",
 | 
			
		||||
  "private": true,
 | 
			
		||||
  "type": "module",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "dev": "vite --port 3000",
 | 
			
		||||
    "start": "vite --port 3000",
 | 
			
		||||
    "build": "vite build && tsc",
 | 
			
		||||
    "serve": "vite preview",
 | 
			
		||||
    "test": "vitest run"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@radix-ui/react-dialog": "^1.1.14",
 | 
			
		||||
    "@radix-ui/react-label": "^2.1.7",
 | 
			
		||||
    "@radix-ui/react-separator": "^1.1.7",
 | 
			
		||||
    "@radix-ui/react-slot": "^1.2.3",
 | 
			
		||||
    "@radix-ui/react-tooltip": "^1.2.7",
 | 
			
		||||
    "@tailwindcss/vite": "^4.1.11",
 | 
			
		||||
    "@tanstack/react-form": "^1.15.0",
 | 
			
		||||
    "@tanstack/react-query": "^5.83.0",
 | 
			
		||||
    "@tanstack/react-router": "^1.121.2",
 | 
			
		||||
    "@tanstack/react-router-devtools": "^1.121.2",
 | 
			
		||||
    "@tanstack/router-plugin": "^1.121.2",
 | 
			
		||||
    "axios": "^1.11.0",
 | 
			
		||||
    "class-variance-authority": "^0.7.1",
 | 
			
		||||
    "clsx": "^2.1.1",
 | 
			
		||||
    "lucide-react": "^0.525.0",
 | 
			
		||||
    "next-themes": "^0.4.6",
 | 
			
		||||
    "react": "^19.0.0",
 | 
			
		||||
    "react-dom": "^19.0.0",
 | 
			
		||||
    "shadcn": "^2.9.3",
 | 
			
		||||
    "sidebar": "^1.0.0",
 | 
			
		||||
    "sonner": "^2.0.7",
 | 
			
		||||
    "tailwind-merge": "^3.3.1",
 | 
			
		||||
    "tailwindcss": "^4.1.11",
 | 
			
		||||
    "zustand": "^5.0.6"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@testing-library/dom": "^10.4.0",
 | 
			
		||||
    "@testing-library/react": "^16.2.0",
 | 
			
		||||
    "@types/node": "^24.1.0",
 | 
			
		||||
    "@types/react": "^19.0.8",
 | 
			
		||||
    "@types/react-dom": "^19.0.3",
 | 
			
		||||
    "@vitejs/plugin-react": "^4.3.4",
 | 
			
		||||
    "jsdom": "^26.0.0",
 | 
			
		||||
    "tw-animate-css": "^1.3.6",
 | 
			
		||||
    "typescript": "^5.7.2",
 | 
			
		||||
    "vite": "^6.1.0",
 | 
			
		||||
    "vitest": "^3.0.5",
 | 
			
		||||
    "web-vitals": "^4.2.4"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								public/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 3.8 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								public/logo192.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/logo192.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 5.2 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								public/logo512.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/logo512.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 9.4 KiB  | 
							
								
								
									
										25
									
								
								public/manifest.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								public/manifest.json
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,25 @@
 | 
			
		|||
{
 | 
			
		||||
  "short_name": "TanStack App",
 | 
			
		||||
  "name": "Create TanStack App Sample",
 | 
			
		||||
  "icons": [
 | 
			
		||||
    {
 | 
			
		||||
      "src": "favicon.ico",
 | 
			
		||||
      "sizes": "64x64 32x32 24x24 16x16",
 | 
			
		||||
      "type": "image/x-icon"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "src": "logo192.png",
 | 
			
		||||
      "type": "image/png",
 | 
			
		||||
      "sizes": "192x192"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "src": "logo512.png",
 | 
			
		||||
      "type": "image/png",
 | 
			
		||||
      "sizes": "512x512"
 | 
			
		||||
    }
 | 
			
		||||
  ],
 | 
			
		||||
  "start_url": ".",
 | 
			
		||||
  "display": "standalone",
 | 
			
		||||
  "theme_color": "#000000",
 | 
			
		||||
  "background_color": "#ffffff"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										3
									
								
								public/robots.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								public/robots.txt
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
# https://www.robotstxt.org/robotstxt.html
 | 
			
		||||
User-agent: *
 | 
			
		||||
Disallow:
 | 
			
		||||
							
								
								
									
										38
									
								
								src/App.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/App.css
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,38 @@
 | 
			
		|||
.App {
 | 
			
		||||
  text-align: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.App-logo {
 | 
			
		||||
  height: 40vmin;
 | 
			
		||||
  pointer-events: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (prefers-reduced-motion: no-preference) {
 | 
			
		||||
  .App-logo {
 | 
			
		||||
    animation: App-logo-spin infinite 20s linear;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.App-header {
 | 
			
		||||
  background-color: #282c34;
 | 
			
		||||
  min-height: 100vh;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  font-size: calc(10px + 2vmin);
 | 
			
		||||
  color: white;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.App-link {
 | 
			
		||||
  color: #61dafb;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes App-logo-spin {
 | 
			
		||||
  from {
 | 
			
		||||
    transform: rotate(0deg);
 | 
			
		||||
  }
 | 
			
		||||
  to {
 | 
			
		||||
    transform: rotate(360deg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										0
									
								
								src/App.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/App.tsx
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										74
									
								
								src/components/app-sidebar.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								src/components/app-sidebar.tsx
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,74 @@
 | 
			
		|||
// app-sidebar.tsx - Đã hỗ trợ onPointerEnter
 | 
			
		||||
import { Link } from "@tanstack/react-router";
 | 
			
		||||
import { Building } from "lucide-react";
 | 
			
		||||
import {
 | 
			
		||||
  Sidebar,
 | 
			
		||||
  SidebarContent,
 | 
			
		||||
  SidebarFooter,
 | 
			
		||||
  SidebarGroup,
 | 
			
		||||
  SidebarGroupContent,
 | 
			
		||||
  SidebarGroupLabel,
 | 
			
		||||
  SidebarHeader,
 | 
			
		||||
  SidebarMenu,
 | 
			
		||||
  SidebarMenuButton,
 | 
			
		||||
  SidebarMenuItem,
 | 
			
		||||
} from "@/components/ui/sidebar";
 | 
			
		||||
 | 
			
		||||
type MenuItem = {
 | 
			
		||||
  title: string;
 | 
			
		||||
  to: string;
 | 
			
		||||
  icon: React.ElementType;
 | 
			
		||||
  onPointerEnter?: () => void;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type AppSidebarProps = {
 | 
			
		||||
  items: MenuItem[];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function AppSidebar({ items }: AppSidebarProps) {
 | 
			
		||||
  return (
 | 
			
		||||
    <Sidebar collapsible="icon" className="border-r">
 | 
			
		||||
      <SidebarHeader className="border-b">
 | 
			
		||||
        <div className="flex items-center gap-2 px-2 py-2">
 | 
			
		||||
          <div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
 | 
			
		||||
            <Building className="size-4" />
 | 
			
		||||
          </div>
 | 
			
		||||
          <div className="flex flex-col gap-0.5 leading-none">
 | 
			
		||||
            <span className="font-semibold">TTMT Computer Management</span>
 | 
			
		||||
            <span className="text-xs text-muted-foreground">v1.0.0</span>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </SidebarHeader>
 | 
			
		||||
      
 | 
			
		||||
      <SidebarContent>
 | 
			
		||||
        <SidebarGroup>
 | 
			
		||||
          <SidebarGroupLabel>Navigation</SidebarGroupLabel>
 | 
			
		||||
          <SidebarGroupContent>
 | 
			
		||||
            <SidebarMenu>
 | 
			
		||||
              {items.map((item) => (
 | 
			
		||||
                <SidebarMenuItem key={item.title}>
 | 
			
		||||
                  <SidebarMenuButton 
 | 
			
		||||
                    asChild 
 | 
			
		||||
                    tooltip={item.title}
 | 
			
		||||
                    onPointerEnter={item.onPointerEnter}
 | 
			
		||||
                  >
 | 
			
		||||
                    <Link href={item.to} to={"."}>
 | 
			
		||||
                      <item.icon className="size-4" />
 | 
			
		||||
                      <span>{item.title}</span>
 | 
			
		||||
                    </Link>
 | 
			
		||||
                  </SidebarMenuButton>
 | 
			
		||||
                </SidebarMenuItem>
 | 
			
		||||
              ))}
 | 
			
		||||
            </SidebarMenu>
 | 
			
		||||
          </SidebarGroupContent>
 | 
			
		||||
        </SidebarGroup>
 | 
			
		||||
      </SidebarContent>
 | 
			
		||||
      
 | 
			
		||||
      <SidebarFooter className="border-t">
 | 
			
		||||
        <div className="p-2 text-xs text-muted-foreground">
 | 
			
		||||
          © 2025 NAVIS Centre 
 | 
			
		||||
        </div>
 | 
			
		||||
      </SidebarFooter>
 | 
			
		||||
    </Sidebar>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										5
									
								
								src/components/submit-button.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/components/submit-button.tsx
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,5 @@
 | 
			
		|||
import { Button } from "./ui/button";
 | 
			
		||||
 | 
			
		||||
export function SubmitButton(){
 | 
			
		||||
    
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										59
									
								
								src/components/ui/button.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								src/components/ui/button.tsx
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,59 @@
 | 
			
		|||
import * as React from "react"
 | 
			
		||||
import { Slot } from "@radix-ui/react-slot"
 | 
			
		||||
import { cva, type VariantProps } from "class-variance-authority"
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
 | 
			
		||||
const buttonVariants = cva(
 | 
			
		||||
  "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
 | 
			
		||||
  {
 | 
			
		||||
    variants: {
 | 
			
		||||
      variant: {
 | 
			
		||||
        default:
 | 
			
		||||
          "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
 | 
			
		||||
        destructive:
 | 
			
		||||
          "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
 | 
			
		||||
        outline:
 | 
			
		||||
          "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
 | 
			
		||||
        secondary:
 | 
			
		||||
          "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
 | 
			
		||||
        ghost:
 | 
			
		||||
          "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
 | 
			
		||||
        link: "text-primary underline-offset-4 hover:underline",
 | 
			
		||||
      },
 | 
			
		||||
      size: {
 | 
			
		||||
        default: "h-9 px-4 py-2 has-[>svg]:px-3",
 | 
			
		||||
        sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
 | 
			
		||||
        lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
 | 
			
		||||
        icon: "size-9",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    defaultVariants: {
 | 
			
		||||
      variant: "default",
 | 
			
		||||
      size: "default",
 | 
			
		||||
    },
 | 
			
		||||
  }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
function Button({
 | 
			
		||||
  className,
 | 
			
		||||
  variant,
 | 
			
		||||
  size,
 | 
			
		||||
  asChild = false,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<"button"> &
 | 
			
		||||
  VariantProps<typeof buttonVariants> & {
 | 
			
		||||
    asChild?: boolean
 | 
			
		||||
  }) {
 | 
			
		||||
  const Comp = asChild ? Slot : "button"
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Comp
 | 
			
		||||
      data-slot="button"
 | 
			
		||||
      className={cn(buttonVariants({ variant, size, className }))}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { Button, buttonVariants }
 | 
			
		||||
							
								
								
									
										92
									
								
								src/components/ui/card.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								src/components/ui/card.tsx
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,92 @@
 | 
			
		|||
import * as React from "react"
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
 | 
			
		||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      data-slot="card"
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
 | 
			
		||||
        className
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      data-slot="card-header"
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
 | 
			
		||||
        className
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      data-slot="card-title"
 | 
			
		||||
      className={cn("leading-none font-semibold", className)}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      data-slot="card-description"
 | 
			
		||||
      className={cn("text-muted-foreground text-sm", className)}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      data-slot="card-action"
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "col-start-2 row-span-2 row-start-1 self-start justify-self-end",
 | 
			
		||||
        className
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      data-slot="card-content"
 | 
			
		||||
      className={cn("px-6", className)}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      data-slot="card-footer"
 | 
			
		||||
      className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  Card,
 | 
			
		||||
  CardHeader,
 | 
			
		||||
  CardFooter,
 | 
			
		||||
  CardTitle,
 | 
			
		||||
  CardAction,
 | 
			
		||||
  CardDescription,
 | 
			
		||||
  CardContent,
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										141
									
								
								src/components/ui/dialog.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								src/components/ui/dialog.tsx
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,141 @@
 | 
			
		|||
import * as React from "react"
 | 
			
		||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
 | 
			
		||||
import { XIcon } from "lucide-react"
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
 | 
			
		||||
function Dialog({
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
 | 
			
		||||
  return <DialogPrimitive.Root data-slot="dialog" {...props} />
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function DialogTrigger({
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
 | 
			
		||||
  return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function DialogPortal({
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
 | 
			
		||||
  return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function DialogClose({
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
 | 
			
		||||
  return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function DialogOverlay({
 | 
			
		||||
  className,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
 | 
			
		||||
  return (
 | 
			
		||||
    <DialogPrimitive.Overlay
 | 
			
		||||
      data-slot="dialog-overlay"
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
 | 
			
		||||
        className
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function DialogContent({
 | 
			
		||||
  className,
 | 
			
		||||
  children,
 | 
			
		||||
  showCloseButton = true,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
 | 
			
		||||
  showCloseButton?: boolean
 | 
			
		||||
}) {
 | 
			
		||||
  return (
 | 
			
		||||
    <DialogPortal data-slot="dialog-portal">
 | 
			
		||||
      <DialogOverlay />
 | 
			
		||||
      <DialogPrimitive.Content
 | 
			
		||||
        data-slot="dialog-content"
 | 
			
		||||
        className={cn(
 | 
			
		||||
          "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
 | 
			
		||||
          className
 | 
			
		||||
        )}
 | 
			
		||||
        {...props}
 | 
			
		||||
      >
 | 
			
		||||
        {children}
 | 
			
		||||
        {showCloseButton && (
 | 
			
		||||
          <DialogPrimitive.Close
 | 
			
		||||
            data-slot="dialog-close"
 | 
			
		||||
            className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
 | 
			
		||||
          >
 | 
			
		||||
            <XIcon />
 | 
			
		||||
            <span className="sr-only">Close</span>
 | 
			
		||||
          </DialogPrimitive.Close>
 | 
			
		||||
        )}
 | 
			
		||||
      </DialogPrimitive.Content>
 | 
			
		||||
    </DialogPortal>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      data-slot="dialog-header"
 | 
			
		||||
      className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      data-slot="dialog-footer"
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
 | 
			
		||||
        className
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function DialogTitle({
 | 
			
		||||
  className,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
 | 
			
		||||
  return (
 | 
			
		||||
    <DialogPrimitive.Title
 | 
			
		||||
      data-slot="dialog-title"
 | 
			
		||||
      className={cn("text-lg leading-none font-semibold", className)}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function DialogDescription({
 | 
			
		||||
  className,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
 | 
			
		||||
  return (
 | 
			
		||||
    <DialogPrimitive.Description
 | 
			
		||||
      data-slot="dialog-description"
 | 
			
		||||
      className={cn("text-muted-foreground text-sm", className)}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  Dialog,
 | 
			
		||||
  DialogClose,
 | 
			
		||||
  DialogContent,
 | 
			
		||||
  DialogDescription,
 | 
			
		||||
  DialogFooter,
 | 
			
		||||
  DialogHeader,
 | 
			
		||||
  DialogOverlay,
 | 
			
		||||
  DialogPortal,
 | 
			
		||||
  DialogTitle,
 | 
			
		||||
  DialogTrigger,
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										21
									
								
								src/components/ui/input.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/components/ui/input.tsx
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,21 @@
 | 
			
		|||
import * as React from "react"
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
 | 
			
		||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
 | 
			
		||||
  return (
 | 
			
		||||
    <input
 | 
			
		||||
      type={type}
 | 
			
		||||
      data-slot="input"
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
 | 
			
		||||
        "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
 | 
			
		||||
        "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
 | 
			
		||||
        className
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { Input }
 | 
			
		||||
							
								
								
									
										22
									
								
								src/components/ui/label.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/components/ui/label.tsx
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,22 @@
 | 
			
		|||
import * as React from "react"
 | 
			
		||||
import * as LabelPrimitive from "@radix-ui/react-label"
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
 | 
			
		||||
function Label({
 | 
			
		||||
  className,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
 | 
			
		||||
  return (
 | 
			
		||||
    <LabelPrimitive.Root
 | 
			
		||||
      data-slot="label"
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
 | 
			
		||||
        className
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { Label }
 | 
			
		||||
							
								
								
									
										28
									
								
								src/components/ui/separator.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/components/ui/separator.tsx
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,28 @@
 | 
			
		|||
"use client"
 | 
			
		||||
 | 
			
		||||
import * as React from "react"
 | 
			
		||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
 | 
			
		||||
function Separator({
 | 
			
		||||
  className,
 | 
			
		||||
  orientation = "horizontal",
 | 
			
		||||
  decorative = true,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
 | 
			
		||||
  return (
 | 
			
		||||
    <SeparatorPrimitive.Root
 | 
			
		||||
      data-slot="separator"
 | 
			
		||||
      decorative={decorative}
 | 
			
		||||
      orientation={orientation}
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
 | 
			
		||||
        className
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { Separator }
 | 
			
		||||
							
								
								
									
										137
									
								
								src/components/ui/sheet.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								src/components/ui/sheet.tsx
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,137 @@
 | 
			
		|||
import * as React from "react"
 | 
			
		||||
import * as SheetPrimitive from "@radix-ui/react-dialog"
 | 
			
		||||
import { XIcon } from "lucide-react"
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
 | 
			
		||||
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
 | 
			
		||||
  return <SheetPrimitive.Root data-slot="sheet" {...props} />
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function SheetTrigger({
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
 | 
			
		||||
  return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function SheetClose({
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
 | 
			
		||||
  return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function SheetPortal({
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
 | 
			
		||||
  return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function SheetOverlay({
 | 
			
		||||
  className,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
 | 
			
		||||
  return (
 | 
			
		||||
    <SheetPrimitive.Overlay
 | 
			
		||||
      data-slot="sheet-overlay"
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
 | 
			
		||||
        className
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function SheetContent({
 | 
			
		||||
  className,
 | 
			
		||||
  children,
 | 
			
		||||
  side = "right",
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
 | 
			
		||||
  side?: "top" | "right" | "bottom" | "left"
 | 
			
		||||
}) {
 | 
			
		||||
  return (
 | 
			
		||||
    <SheetPortal>
 | 
			
		||||
      <SheetOverlay />
 | 
			
		||||
      <SheetPrimitive.Content
 | 
			
		||||
        data-slot="sheet-content"
 | 
			
		||||
        className={cn(
 | 
			
		||||
          "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
 | 
			
		||||
          side === "right" &&
 | 
			
		||||
            "data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
 | 
			
		||||
          side === "left" &&
 | 
			
		||||
            "data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
 | 
			
		||||
          side === "top" &&
 | 
			
		||||
            "data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
 | 
			
		||||
          side === "bottom" &&
 | 
			
		||||
            "data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
 | 
			
		||||
          className
 | 
			
		||||
        )}
 | 
			
		||||
        {...props}
 | 
			
		||||
      >
 | 
			
		||||
        {children}
 | 
			
		||||
        <SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
 | 
			
		||||
          <XIcon className="size-4" />
 | 
			
		||||
          <span className="sr-only">Close</span>
 | 
			
		||||
        </SheetPrimitive.Close>
 | 
			
		||||
      </SheetPrimitive.Content>
 | 
			
		||||
    </SheetPortal>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      data-slot="sheet-header"
 | 
			
		||||
      className={cn("flex flex-col gap-1.5 p-4", className)}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      data-slot="sheet-footer"
 | 
			
		||||
      className={cn("mt-auto flex flex-col gap-2 p-4", className)}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function SheetTitle({
 | 
			
		||||
  className,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
 | 
			
		||||
  return (
 | 
			
		||||
    <SheetPrimitive.Title
 | 
			
		||||
      data-slot="sheet-title"
 | 
			
		||||
      className={cn("text-foreground font-semibold", className)}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function SheetDescription({
 | 
			
		||||
  className,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
 | 
			
		||||
  return (
 | 
			
		||||
    <SheetPrimitive.Description
 | 
			
		||||
      data-slot="sheet-description"
 | 
			
		||||
      className={cn("text-muted-foreground text-sm", className)}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  Sheet,
 | 
			
		||||
  SheetTrigger,
 | 
			
		||||
  SheetClose,
 | 
			
		||||
  SheetContent,
 | 
			
		||||
  SheetHeader,
 | 
			
		||||
  SheetFooter,
 | 
			
		||||
  SheetTitle,
 | 
			
		||||
  SheetDescription,
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										724
									
								
								src/components/ui/sidebar.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										724
									
								
								src/components/ui/sidebar.tsx
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,724 @@
 | 
			
		|||
import * as React from "react"
 | 
			
		||||
import { Slot } from "@radix-ui/react-slot"
 | 
			
		||||
import { cva, type VariantProps } from "class-variance-authority"
 | 
			
		||||
import { PanelLeftIcon } from "lucide-react"
 | 
			
		||||
 | 
			
		||||
import { useIsMobile } from "@/hooks/use-mobile"
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
import { Button } from "@/components/ui/button"
 | 
			
		||||
import { Input } from "@/components/ui/input"
 | 
			
		||||
import { Separator } from "@/components/ui/separator"
 | 
			
		||||
import {
 | 
			
		||||
  Sheet,
 | 
			
		||||
  SheetContent,
 | 
			
		||||
  SheetDescription,
 | 
			
		||||
  SheetHeader,
 | 
			
		||||
  SheetTitle,
 | 
			
		||||
} from "@/components/ui/sheet"
 | 
			
		||||
import { Skeleton } from "@/components/ui/skeleton"
 | 
			
		||||
import {
 | 
			
		||||
  Tooltip,
 | 
			
		||||
  TooltipContent,
 | 
			
		||||
  TooltipProvider,
 | 
			
		||||
  TooltipTrigger,
 | 
			
		||||
} from "@/components/ui/tooltip"
 | 
			
		||||
 | 
			
		||||
const SIDEBAR_COOKIE_NAME = "sidebar_state"
 | 
			
		||||
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
 | 
			
		||||
const SIDEBAR_WIDTH = "16rem"
 | 
			
		||||
const SIDEBAR_WIDTH_MOBILE = "18rem"
 | 
			
		||||
const SIDEBAR_WIDTH_ICON = "3rem"
 | 
			
		||||
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
 | 
			
		||||
 | 
			
		||||
type SidebarContextProps = {
 | 
			
		||||
  state: "expanded" | "collapsed"
 | 
			
		||||
  open: boolean
 | 
			
		||||
  setOpen: (open: boolean) => void
 | 
			
		||||
  openMobile: boolean
 | 
			
		||||
  setOpenMobile: (open: boolean) => void
 | 
			
		||||
  isMobile: boolean
 | 
			
		||||
  toggleSidebar: () => void
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const SidebarContext = React.createContext<SidebarContextProps | null>(null)
 | 
			
		||||
 | 
			
		||||
function useSidebar() {
 | 
			
		||||
  const context = React.useContext(SidebarContext)
 | 
			
		||||
  if (!context) {
 | 
			
		||||
    throw new Error("useSidebar must be used within a SidebarProvider.")
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return context
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function SidebarProvider({
 | 
			
		||||
  defaultOpen = true,
 | 
			
		||||
  open: openProp,
 | 
			
		||||
  onOpenChange: setOpenProp,
 | 
			
		||||
  className,
 | 
			
		||||
  style,
 | 
			
		||||
  children,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<"div"> & {
 | 
			
		||||
  defaultOpen?: boolean
 | 
			
		||||
  open?: boolean
 | 
			
		||||
  onOpenChange?: (open: boolean) => void
 | 
			
		||||
}) {
 | 
			
		||||
  const isMobile = useIsMobile()
 | 
			
		||||
  const [openMobile, setOpenMobile] = React.useState(false)
 | 
			
		||||
 | 
			
		||||
  // This is the internal state of the sidebar.
 | 
			
		||||
  // We use openProp and setOpenProp for control from outside the component.
 | 
			
		||||
  const [_open, _setOpen] = React.useState(defaultOpen)
 | 
			
		||||
  const open = openProp ?? _open
 | 
			
		||||
  const setOpen = React.useCallback(
 | 
			
		||||
    (value: boolean | ((value: boolean) => boolean)) => {
 | 
			
		||||
      const openState = typeof value === "function" ? value(open) : value
 | 
			
		||||
      if (setOpenProp) {
 | 
			
		||||
        setOpenProp(openState)
 | 
			
		||||
      } else {
 | 
			
		||||
        _setOpen(openState)
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // This sets the cookie to keep the sidebar state.
 | 
			
		||||
      document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
 | 
			
		||||
    },
 | 
			
		||||
    [setOpenProp, open]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  // Helper to toggle the sidebar.
 | 
			
		||||
  const toggleSidebar = React.useCallback(() => {
 | 
			
		||||
    return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)
 | 
			
		||||
  }, [isMobile, setOpen, setOpenMobile])
 | 
			
		||||
 | 
			
		||||
  // Adds a keyboard shortcut to toggle the sidebar.
 | 
			
		||||
  React.useEffect(() => {
 | 
			
		||||
    const handleKeyDown = (event: KeyboardEvent) => {
 | 
			
		||||
      if (
 | 
			
		||||
        event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
 | 
			
		||||
        (event.metaKey || event.ctrlKey)
 | 
			
		||||
      ) {
 | 
			
		||||
        event.preventDefault()
 | 
			
		||||
        toggleSidebar()
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    window.addEventListener("keydown", handleKeyDown)
 | 
			
		||||
    return () => window.removeEventListener("keydown", handleKeyDown)
 | 
			
		||||
  }, [toggleSidebar])
 | 
			
		||||
 | 
			
		||||
  // We add a state so that we can do data-state="expanded" or "collapsed".
 | 
			
		||||
  // This makes it easier to style the sidebar with Tailwind classes.
 | 
			
		||||
  const state = open ? "expanded" : "collapsed"
 | 
			
		||||
 | 
			
		||||
  const contextValue = React.useMemo<SidebarContextProps>(
 | 
			
		||||
    () => ({
 | 
			
		||||
      state,
 | 
			
		||||
      open,
 | 
			
		||||
      setOpen,
 | 
			
		||||
      isMobile,
 | 
			
		||||
      openMobile,
 | 
			
		||||
      setOpenMobile,
 | 
			
		||||
      toggleSidebar,
 | 
			
		||||
    }),
 | 
			
		||||
    [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <SidebarContext.Provider value={contextValue}>
 | 
			
		||||
      <TooltipProvider delayDuration={0}>
 | 
			
		||||
        <div
 | 
			
		||||
          data-slot="sidebar-wrapper"
 | 
			
		||||
          style={
 | 
			
		||||
            {
 | 
			
		||||
              "--sidebar-width": SIDEBAR_WIDTH,
 | 
			
		||||
              "--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
 | 
			
		||||
              ...style,
 | 
			
		||||
            } as React.CSSProperties
 | 
			
		||||
          }
 | 
			
		||||
          className={cn(
 | 
			
		||||
            "group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full",
 | 
			
		||||
            className
 | 
			
		||||
          )}
 | 
			
		||||
          {...props}
 | 
			
		||||
        >
 | 
			
		||||
          {children}
 | 
			
		||||
        </div>
 | 
			
		||||
      </TooltipProvider>
 | 
			
		||||
    </SidebarContext.Provider>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function Sidebar({
 | 
			
		||||
  side = "left",
 | 
			
		||||
  variant = "sidebar",
 | 
			
		||||
  collapsible = "offcanvas",
 | 
			
		||||
  className,
 | 
			
		||||
  children,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<"div"> & {
 | 
			
		||||
  side?: "left" | "right"
 | 
			
		||||
  variant?: "sidebar" | "floating" | "inset"
 | 
			
		||||
  collapsible?: "offcanvas" | "icon" | "none"
 | 
			
		||||
}) {
 | 
			
		||||
  const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
 | 
			
		||||
 | 
			
		||||
  if (collapsible === "none") {
 | 
			
		||||
    return (
 | 
			
		||||
      <div
 | 
			
		||||
        data-slot="sidebar"
 | 
			
		||||
        className={cn(
 | 
			
		||||
          "bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col",
 | 
			
		||||
          className
 | 
			
		||||
        )}
 | 
			
		||||
        {...props}
 | 
			
		||||
      >
 | 
			
		||||
        {children}
 | 
			
		||||
      </div>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (isMobile) {
 | 
			
		||||
    return (
 | 
			
		||||
      <Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
 | 
			
		||||
        <SheetContent
 | 
			
		||||
          data-sidebar="sidebar"
 | 
			
		||||
          data-slot="sidebar"
 | 
			
		||||
          data-mobile="true"
 | 
			
		||||
          className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
 | 
			
		||||
          style={
 | 
			
		||||
            {
 | 
			
		||||
              "--sidebar-width": SIDEBAR_WIDTH_MOBILE,
 | 
			
		||||
            } as React.CSSProperties
 | 
			
		||||
          }
 | 
			
		||||
          side={side}
 | 
			
		||||
        >
 | 
			
		||||
          <SheetHeader className="sr-only">
 | 
			
		||||
            <SheetTitle>Sidebar</SheetTitle>
 | 
			
		||||
            <SheetDescription>Displays the mobile sidebar.</SheetDescription>
 | 
			
		||||
          </SheetHeader>
 | 
			
		||||
          <div className="flex h-full w-full flex-col">{children}</div>
 | 
			
		||||
        </SheetContent>
 | 
			
		||||
      </Sheet>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      className="group peer text-sidebar-foreground hidden md:block"
 | 
			
		||||
      data-state={state}
 | 
			
		||||
      data-collapsible={state === "collapsed" ? collapsible : ""}
 | 
			
		||||
      data-variant={variant}
 | 
			
		||||
      data-side={side}
 | 
			
		||||
      data-slot="sidebar"
 | 
			
		||||
    >
 | 
			
		||||
      {/* This is what handles the sidebar gap on desktop */}
 | 
			
		||||
      <div
 | 
			
		||||
        data-slot="sidebar-gap"
 | 
			
		||||
        className={cn(
 | 
			
		||||
          "relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear",
 | 
			
		||||
          "group-data-[collapsible=offcanvas]:w-0",
 | 
			
		||||
          "group-data-[side=right]:rotate-180",
 | 
			
		||||
          variant === "floating" || variant === "inset"
 | 
			
		||||
            ? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
 | 
			
		||||
            : "group-data-[collapsible=icon]:w-(--sidebar-width-icon)"
 | 
			
		||||
        )}
 | 
			
		||||
      />
 | 
			
		||||
      <div
 | 
			
		||||
        data-slot="sidebar-container"
 | 
			
		||||
        className={cn(
 | 
			
		||||
          "fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex",
 | 
			
		||||
          side === "left"
 | 
			
		||||
            ? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
 | 
			
		||||
            : "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
 | 
			
		||||
          // Adjust the padding for floating and inset variants.
 | 
			
		||||
          variant === "floating" || variant === "inset"
 | 
			
		||||
            ? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
 | 
			
		||||
            : "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
 | 
			
		||||
          className
 | 
			
		||||
        )}
 | 
			
		||||
        {...props}
 | 
			
		||||
      >
 | 
			
		||||
        <div
 | 
			
		||||
          data-sidebar="sidebar"
 | 
			
		||||
          data-slot="sidebar-inner"
 | 
			
		||||
          className="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
 | 
			
		||||
        >
 | 
			
		||||
          {children}
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function SidebarTrigger({
 | 
			
		||||
  className,
 | 
			
		||||
  onClick,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<typeof Button>) {
 | 
			
		||||
  const { toggleSidebar } = useSidebar()
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Button
 | 
			
		||||
      data-sidebar="trigger"
 | 
			
		||||
      data-slot="sidebar-trigger"
 | 
			
		||||
      variant="ghost"
 | 
			
		||||
      size="icon"
 | 
			
		||||
      className={cn("size-7", className)}
 | 
			
		||||
      onClick={(event) => {
 | 
			
		||||
        onClick?.(event)
 | 
			
		||||
        toggleSidebar()
 | 
			
		||||
      }}
 | 
			
		||||
      {...props}
 | 
			
		||||
    >
 | 
			
		||||
      <PanelLeftIcon />
 | 
			
		||||
      <span className="sr-only">Toggle Sidebar</span>
 | 
			
		||||
    </Button>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
 | 
			
		||||
  const { toggleSidebar } = useSidebar()
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <button
 | 
			
		||||
      data-sidebar="rail"
 | 
			
		||||
      data-slot="sidebar-rail"
 | 
			
		||||
      aria-label="Toggle Sidebar"
 | 
			
		||||
      tabIndex={-1}
 | 
			
		||||
      onClick={toggleSidebar}
 | 
			
		||||
      title="Toggle Sidebar"
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex",
 | 
			
		||||
        "in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
 | 
			
		||||
        "[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
 | 
			
		||||
        "hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
 | 
			
		||||
        "[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
 | 
			
		||||
        "[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
 | 
			
		||||
        className
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
 | 
			
		||||
  return (
 | 
			
		||||
    <main
 | 
			
		||||
      data-slot="sidebar-inset"
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "bg-background relative flex w-full flex-1 flex-col",
 | 
			
		||||
        "md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
 | 
			
		||||
        className
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function SidebarInput({
 | 
			
		||||
  className,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<typeof Input>) {
 | 
			
		||||
  return (
 | 
			
		||||
    <Input
 | 
			
		||||
      data-slot="sidebar-input"
 | 
			
		||||
      data-sidebar="input"
 | 
			
		||||
      className={cn("bg-background h-8 w-full shadow-none", className)}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      data-slot="sidebar-header"
 | 
			
		||||
      data-sidebar="header"
 | 
			
		||||
      className={cn("flex flex-col gap-2 p-2", className)}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      data-slot="sidebar-footer"
 | 
			
		||||
      data-sidebar="footer"
 | 
			
		||||
      className={cn("flex flex-col gap-2 p-2", className)}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function SidebarSeparator({
 | 
			
		||||
  className,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<typeof Separator>) {
 | 
			
		||||
  return (
 | 
			
		||||
    <Separator
 | 
			
		||||
      data-slot="sidebar-separator"
 | 
			
		||||
      data-sidebar="separator"
 | 
			
		||||
      className={cn("bg-sidebar-border mx-2 w-auto", className)}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      data-slot="sidebar-content"
 | 
			
		||||
      data-sidebar="content"
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
 | 
			
		||||
        className
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      data-slot="sidebar-group"
 | 
			
		||||
      data-sidebar="group"
 | 
			
		||||
      className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function SidebarGroupLabel({
 | 
			
		||||
  className,
 | 
			
		||||
  asChild = false,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<"div"> & { asChild?: boolean }) {
 | 
			
		||||
  const Comp = asChild ? Slot : "div"
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Comp
 | 
			
		||||
      data-slot="sidebar-group-label"
 | 
			
		||||
      data-sidebar="group-label"
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
 | 
			
		||||
        "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
 | 
			
		||||
        className
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function SidebarGroupAction({
 | 
			
		||||
  className,
 | 
			
		||||
  asChild = false,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<"button"> & { asChild?: boolean }) {
 | 
			
		||||
  const Comp = asChild ? Slot : "button"
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Comp
 | 
			
		||||
      data-slot="sidebar-group-action"
 | 
			
		||||
      data-sidebar="group-action"
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
 | 
			
		||||
        // Increases the hit area of the button on mobile.
 | 
			
		||||
        "after:absolute after:-inset-2 md:after:hidden",
 | 
			
		||||
        "group-data-[collapsible=icon]:hidden",
 | 
			
		||||
        className
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function SidebarGroupContent({
 | 
			
		||||
  className,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<"div">) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      data-slot="sidebar-group-content"
 | 
			
		||||
      data-sidebar="group-content"
 | 
			
		||||
      className={cn("w-full text-sm", className)}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
 | 
			
		||||
  return (
 | 
			
		||||
    <ul
 | 
			
		||||
      data-slot="sidebar-menu"
 | 
			
		||||
      data-sidebar="menu"
 | 
			
		||||
      className={cn("flex w-full min-w-0 flex-col gap-1", className)}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
 | 
			
		||||
  return (
 | 
			
		||||
    <li
 | 
			
		||||
      data-slot="sidebar-menu-item"
 | 
			
		||||
      data-sidebar="menu-item"
 | 
			
		||||
      className={cn("group/menu-item relative", className)}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const sidebarMenuButtonVariants = cva(
 | 
			
		||||
  "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
 | 
			
		||||
  {
 | 
			
		||||
    variants: {
 | 
			
		||||
      variant: {
 | 
			
		||||
        default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
 | 
			
		||||
        outline:
 | 
			
		||||
          "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
 | 
			
		||||
      },
 | 
			
		||||
      size: {
 | 
			
		||||
        default: "h-8 text-sm",
 | 
			
		||||
        sm: "h-7 text-xs",
 | 
			
		||||
        lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    defaultVariants: {
 | 
			
		||||
      variant: "default",
 | 
			
		||||
      size: "default",
 | 
			
		||||
    },
 | 
			
		||||
  }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
function SidebarMenuButton({
 | 
			
		||||
  asChild = false,
 | 
			
		||||
  isActive = false,
 | 
			
		||||
  variant = "default",
 | 
			
		||||
  size = "default",
 | 
			
		||||
  tooltip,
 | 
			
		||||
  className,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<"button"> & {
 | 
			
		||||
  asChild?: boolean
 | 
			
		||||
  isActive?: boolean
 | 
			
		||||
  tooltip?: string | React.ComponentProps<typeof TooltipContent>
 | 
			
		||||
} & VariantProps<typeof sidebarMenuButtonVariants>) {
 | 
			
		||||
  const Comp = asChild ? Slot : "button"
 | 
			
		||||
  const { isMobile, state } = useSidebar()
 | 
			
		||||
 | 
			
		||||
  const button = (
 | 
			
		||||
    <Comp
 | 
			
		||||
      data-slot="sidebar-menu-button"
 | 
			
		||||
      data-sidebar="menu-button"
 | 
			
		||||
      data-size={size}
 | 
			
		||||
      data-active={isActive}
 | 
			
		||||
      className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  if (!tooltip) {
 | 
			
		||||
    return button
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (typeof tooltip === "string") {
 | 
			
		||||
    tooltip = {
 | 
			
		||||
      children: tooltip,
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Tooltip>
 | 
			
		||||
      <TooltipTrigger asChild>{button}</TooltipTrigger>
 | 
			
		||||
      <TooltipContent
 | 
			
		||||
        side="right"
 | 
			
		||||
        align="center"
 | 
			
		||||
        hidden={state !== "collapsed" || isMobile}
 | 
			
		||||
        {...tooltip}
 | 
			
		||||
      />
 | 
			
		||||
    </Tooltip>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function SidebarMenuAction({
 | 
			
		||||
  className,
 | 
			
		||||
  asChild = false,
 | 
			
		||||
  showOnHover = false,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<"button"> & {
 | 
			
		||||
  asChild?: boolean
 | 
			
		||||
  showOnHover?: boolean
 | 
			
		||||
}) {
 | 
			
		||||
  const Comp = asChild ? Slot : "button"
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Comp
 | 
			
		||||
      data-slot="sidebar-menu-action"
 | 
			
		||||
      data-sidebar="menu-action"
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
 | 
			
		||||
        // Increases the hit area of the button on mobile.
 | 
			
		||||
        "after:absolute after:-inset-2 md:after:hidden",
 | 
			
		||||
        "peer-data-[size=sm]/menu-button:top-1",
 | 
			
		||||
        "peer-data-[size=default]/menu-button:top-1.5",
 | 
			
		||||
        "peer-data-[size=lg]/menu-button:top-2.5",
 | 
			
		||||
        "group-data-[collapsible=icon]:hidden",
 | 
			
		||||
        showOnHover &&
 | 
			
		||||
          "peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
 | 
			
		||||
        className
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function SidebarMenuBadge({
 | 
			
		||||
  className,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<"div">) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      data-slot="sidebar-menu-badge"
 | 
			
		||||
      data-sidebar="menu-badge"
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none",
 | 
			
		||||
        "peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
 | 
			
		||||
        "peer-data-[size=sm]/menu-button:top-1",
 | 
			
		||||
        "peer-data-[size=default]/menu-button:top-1.5",
 | 
			
		||||
        "peer-data-[size=lg]/menu-button:top-2.5",
 | 
			
		||||
        "group-data-[collapsible=icon]:hidden",
 | 
			
		||||
        className
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function SidebarMenuSkeleton({
 | 
			
		||||
  className,
 | 
			
		||||
  showIcon = false,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<"div"> & {
 | 
			
		||||
  showIcon?: boolean
 | 
			
		||||
}) {
 | 
			
		||||
  // Random width between 50 to 90%.
 | 
			
		||||
  const width = React.useMemo(() => {
 | 
			
		||||
    return `${Math.floor(Math.random() * 40) + 50}%`
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      data-slot="sidebar-menu-skeleton"
 | 
			
		||||
      data-sidebar="menu-skeleton"
 | 
			
		||||
      className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
 | 
			
		||||
      {...props}
 | 
			
		||||
    >
 | 
			
		||||
      {showIcon && (
 | 
			
		||||
        <Skeleton
 | 
			
		||||
          className="size-4 rounded-md"
 | 
			
		||||
          data-sidebar="menu-skeleton-icon"
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
      <Skeleton
 | 
			
		||||
        className="h-4 max-w-(--skeleton-width) flex-1"
 | 
			
		||||
        data-sidebar="menu-skeleton-text"
 | 
			
		||||
        style={
 | 
			
		||||
          {
 | 
			
		||||
            "--skeleton-width": width,
 | 
			
		||||
          } as React.CSSProperties
 | 
			
		||||
        }
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
 | 
			
		||||
  return (
 | 
			
		||||
    <ul
 | 
			
		||||
      data-slot="sidebar-menu-sub"
 | 
			
		||||
      data-sidebar="menu-sub"
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",
 | 
			
		||||
        "group-data-[collapsible=icon]:hidden",
 | 
			
		||||
        className
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function SidebarMenuSubItem({
 | 
			
		||||
  className,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<"li">) {
 | 
			
		||||
  return (
 | 
			
		||||
    <li
 | 
			
		||||
      data-slot="sidebar-menu-sub-item"
 | 
			
		||||
      data-sidebar="menu-sub-item"
 | 
			
		||||
      className={cn("group/menu-sub-item relative", className)}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function SidebarMenuSubButton({
 | 
			
		||||
  asChild = false,
 | 
			
		||||
  size = "md",
 | 
			
		||||
  isActive = false,
 | 
			
		||||
  className,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<"a"> & {
 | 
			
		||||
  asChild?: boolean
 | 
			
		||||
  size?: "sm" | "md"
 | 
			
		||||
  isActive?: boolean
 | 
			
		||||
}) {
 | 
			
		||||
  const Comp = asChild ? Slot : "a"
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Comp
 | 
			
		||||
      data-slot="sidebar-menu-sub-button"
 | 
			
		||||
      data-sidebar="menu-sub-button"
 | 
			
		||||
      data-size={size}
 | 
			
		||||
      data-active={isActive}
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
 | 
			
		||||
        "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
 | 
			
		||||
        size === "sm" && "text-xs",
 | 
			
		||||
        size === "md" && "text-sm",
 | 
			
		||||
        "group-data-[collapsible=icon]:hidden",
 | 
			
		||||
        className
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  Sidebar,
 | 
			
		||||
  SidebarContent,
 | 
			
		||||
  SidebarFooter,
 | 
			
		||||
  SidebarGroup,
 | 
			
		||||
  SidebarGroupAction,
 | 
			
		||||
  SidebarGroupContent,
 | 
			
		||||
  SidebarGroupLabel,
 | 
			
		||||
  SidebarHeader,
 | 
			
		||||
  SidebarInput,
 | 
			
		||||
  SidebarInset,
 | 
			
		||||
  SidebarMenu,
 | 
			
		||||
  SidebarMenuAction,
 | 
			
		||||
  SidebarMenuBadge,
 | 
			
		||||
  SidebarMenuButton,
 | 
			
		||||
  SidebarMenuItem,
 | 
			
		||||
  SidebarMenuSkeleton,
 | 
			
		||||
  SidebarMenuSub,
 | 
			
		||||
  SidebarMenuSubButton,
 | 
			
		||||
  SidebarMenuSubItem,
 | 
			
		||||
  SidebarProvider,
 | 
			
		||||
  SidebarRail,
 | 
			
		||||
  SidebarSeparator,
 | 
			
		||||
  SidebarTrigger,
 | 
			
		||||
  useSidebar,
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										13
									
								
								src/components/ui/skeleton.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/components/ui/skeleton.tsx
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,13 @@
 | 
			
		|||
import { cn } from "@/lib/utils"
 | 
			
		||||
 | 
			
		||||
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      data-slot="skeleton"
 | 
			
		||||
      className={cn("bg-accent animate-pulse rounded-md", className)}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { Skeleton }
 | 
			
		||||
							
								
								
									
										23
									
								
								src/components/ui/sonner.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/components/ui/sonner.tsx
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,23 @@
 | 
			
		|||
import { useTheme } from "next-themes"
 | 
			
		||||
import { Toaster as Sonner, type ToasterProps } from "sonner"
 | 
			
		||||
 | 
			
		||||
const Toaster = ({ ...props }: ToasterProps) => {
 | 
			
		||||
  const { theme = "system" } = useTheme()
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Sonner
 | 
			
		||||
      theme={theme as ToasterProps["theme"]}
 | 
			
		||||
      className="toaster group"
 | 
			
		||||
      style={
 | 
			
		||||
        {
 | 
			
		||||
          "--normal-bg": "var(--popover)",
 | 
			
		||||
          "--normal-text": "var(--popover-foreground)",
 | 
			
		||||
          "--normal-border": "var(--border)",
 | 
			
		||||
        } as React.CSSProperties
 | 
			
		||||
      }
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { Toaster }
 | 
			
		||||
							
								
								
									
										114
									
								
								src/components/ui/table.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								src/components/ui/table.tsx
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,114 @@
 | 
			
		|||
import * as React from "react"
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
 | 
			
		||||
function Table({ className, ...props }: React.ComponentProps<"table">) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      data-slot="table-container"
 | 
			
		||||
      className="relative w-full overflow-x-auto"
 | 
			
		||||
    >
 | 
			
		||||
      <table
 | 
			
		||||
        data-slot="table"
 | 
			
		||||
        className={cn("w-full caption-bottom text-sm", className)}
 | 
			
		||||
        {...props}
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
 | 
			
		||||
  return (
 | 
			
		||||
    <thead
 | 
			
		||||
      data-slot="table-header"
 | 
			
		||||
      className={cn("[&_tr]:border-b", className)}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
 | 
			
		||||
  return (
 | 
			
		||||
    <tbody
 | 
			
		||||
      data-slot="table-body"
 | 
			
		||||
      className={cn("[&_tr:last-child]:border-0", className)}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
 | 
			
		||||
  return (
 | 
			
		||||
    <tfoot
 | 
			
		||||
      data-slot="table-footer"
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
 | 
			
		||||
        className
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
 | 
			
		||||
  return (
 | 
			
		||||
    <tr
 | 
			
		||||
      data-slot="table-row"
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
 | 
			
		||||
        className
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
 | 
			
		||||
  return (
 | 
			
		||||
    <th
 | 
			
		||||
      data-slot="table-head"
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
 | 
			
		||||
        className
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
 | 
			
		||||
  return (
 | 
			
		||||
    <td
 | 
			
		||||
      data-slot="table-cell"
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
 | 
			
		||||
        className
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function TableCaption({
 | 
			
		||||
  className,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<"caption">) {
 | 
			
		||||
  return (
 | 
			
		||||
    <caption
 | 
			
		||||
      data-slot="table-caption"
 | 
			
		||||
      className={cn("text-muted-foreground mt-4 text-sm", className)}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  Table,
 | 
			
		||||
  TableHeader,
 | 
			
		||||
  TableBody,
 | 
			
		||||
  TableFooter,
 | 
			
		||||
  TableHead,
 | 
			
		||||
  TableRow,
 | 
			
		||||
  TableCell,
 | 
			
		||||
  TableCaption,
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										18
									
								
								src/components/ui/textarea.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/components/ui/textarea.tsx
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,18 @@
 | 
			
		|||
import * as React from "react"
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
 | 
			
		||||
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
 | 
			
		||||
  return (
 | 
			
		||||
    <textarea
 | 
			
		||||
      data-slot="textarea"
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
 | 
			
		||||
        className
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { Textarea }
 | 
			
		||||
							
								
								
									
										61
									
								
								src/components/ui/tooltip.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								src/components/ui/tooltip.tsx
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,61 @@
 | 
			
		|||
"use client"
 | 
			
		||||
 | 
			
		||||
import * as React from "react"
 | 
			
		||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
 | 
			
		||||
function TooltipProvider({
 | 
			
		||||
  delayDuration = 0,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
 | 
			
		||||
  return (
 | 
			
		||||
    <TooltipPrimitive.Provider
 | 
			
		||||
      data-slot="tooltip-provider"
 | 
			
		||||
      delayDuration={delayDuration}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function Tooltip({
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
 | 
			
		||||
  return (
 | 
			
		||||
    <TooltipProvider>
 | 
			
		||||
      <TooltipPrimitive.Root data-slot="tooltip" {...props} />
 | 
			
		||||
    </TooltipProvider>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function TooltipTrigger({
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
 | 
			
		||||
  return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function TooltipContent({
 | 
			
		||||
  className,
 | 
			
		||||
  sideOffset = 0,
 | 
			
		||||
  children,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
 | 
			
		||||
  return (
 | 
			
		||||
    <TooltipPrimitive.Portal>
 | 
			
		||||
      <TooltipPrimitive.Content
 | 
			
		||||
        data-slot="tooltip-content"
 | 
			
		||||
        sideOffset={sideOffset}
 | 
			
		||||
        className={cn(
 | 
			
		||||
          "bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
 | 
			
		||||
          className
 | 
			
		||||
        )}
 | 
			
		||||
        {...props}
 | 
			
		||||
      >
 | 
			
		||||
        {children}
 | 
			
		||||
        <TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
 | 
			
		||||
      </TooltipPrimitive.Content>
 | 
			
		||||
    </TooltipPrimitive.Portal>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
 | 
			
		||||
							
								
								
									
										23
									
								
								src/config/api.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/config/api.ts
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,23 @@
 | 
			
		|||
export const API_MODE = import.meta.env.VITE_API_MODE as "tailscale" | "local" | "ip";
 | 
			
		||||
 | 
			
		||||
export const API_ROOT = {
 | 
			
		||||
  tailscale: import.meta.env.VITE_API_URL_TAILSCALE,
 | 
			
		||||
  local: import.meta.env.VITE_API_URL_LOCAL,
 | 
			
		||||
  ip: import.meta.env.VITE_API_URL_IP,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
console.log(API_ROOT)
 | 
			
		||||
 | 
			
		||||
export const BASE_URL = API_ROOT[API_MODE];
 | 
			
		||||
 | 
			
		||||
export const API_ENDPOINTS = {
 | 
			
		||||
  APP_VERSION: {
 | 
			
		||||
    GET_VERSION: `/AppVersion/version`,
 | 
			
		||||
    UPLOAD: `/AppVersion/upload`,
 | 
			
		||||
  },
 | 
			
		||||
  DEVICE_COMM: {
 | 
			
		||||
    UPDATE_AGENT: `/DeviceComm/updateagent`,
 | 
			
		||||
    GET_ROOM_LIST: `/DeviceComm/rooms`,
 | 
			
		||||
    GET_DEVICE_FROM_ROOM: (roomName: string) => `/DeviceComm/room/${roomName}`,
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										19
									
								
								src/hooks/use-mobile.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/hooks/use-mobile.ts
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,19 @@
 | 
			
		|||
import * as React from "react"
 | 
			
		||||
 | 
			
		||||
const MOBILE_BREAKPOINT = 768
 | 
			
		||||
 | 
			
		||||
export function useIsMobile() {
 | 
			
		||||
  const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
 | 
			
		||||
 | 
			
		||||
  React.useEffect(() => {
 | 
			
		||||
    const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
 | 
			
		||||
    const onChange = () => {
 | 
			
		||||
      setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
 | 
			
		||||
    }
 | 
			
		||||
    mql.addEventListener("change", onChange)
 | 
			
		||||
    setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
 | 
			
		||||
    return () => mql.removeEventListener("change", onChange)
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  return !!isMobile
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										37
									
								
								src/hooks/useAuthtoken.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/hooks/useAuthtoken.ts
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,37 @@
 | 
			
		|||
import {create} from 'zustand'
 | 
			
		||||
import {persist} from 'zustand/middleware'
 | 
			
		||||
 | 
			
		||||
type State = {
 | 
			
		||||
  authToken?: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Action = {
 | 
			
		||||
  setAuthToken: (token: string) => void
 | 
			
		||||
  removeAuthToken: () => void
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type AuthTokenProps = State & Action
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
const initialState: State = {
 | 
			
		||||
  authToken: undefined,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const useAuthToken = create<AuthTokenProps>()(
 | 
			
		||||
  persist(
 | 
			
		||||
    set => ({
 | 
			
		||||
      ...initialState,
 | 
			
		||||
      setAuthToken: token => {
 | 
			
		||||
        set({authToken: token})
 | 
			
		||||
      },
 | 
			
		||||
      removeAuthToken: () => {
 | 
			
		||||
        set(initialState)
 | 
			
		||||
      },
 | 
			
		||||
    }),
 | 
			
		||||
    {
 | 
			
		||||
      name: 'auth',
 | 
			
		||||
    },
 | 
			
		||||
  ),
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
export default useAuthToken
 | 
			
		||||
							
								
								
									
										39
									
								
								src/hooks/useMutationData.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/hooks/useMutationData.ts
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,39 @@
 | 
			
		|||
import { useMutation, useQueryClient } from "@tanstack/react-query"
 | 
			
		||||
import axios, { type Method } from "axios"
 | 
			
		||||
 | 
			
		||||
type MutationDataOptions<TInput, TOutput> = {
 | 
			
		||||
  url: string
 | 
			
		||||
  method?: Method // POST, PUT, PATCH, DELETE
 | 
			
		||||
  onSuccess?: (data: TOutput) => void
 | 
			
		||||
  onError?: (error: any) => void
 | 
			
		||||
  invalidate?: string[][] // List of queryKeys to invalidate
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function useMutationData<TInput = any, TOutput = any>({
 | 
			
		||||
  url,
 | 
			
		||||
  method = "POST",
 | 
			
		||||
  onSuccess,
 | 
			
		||||
  onError,
 | 
			
		||||
  invalidate = [],
 | 
			
		||||
}: MutationDataOptions<TInput, TOutput>) {
 | 
			
		||||
  const queryClient = useQueryClient()
 | 
			
		||||
 | 
			
		||||
  return useMutation<TOutput, any, TInput>({
 | 
			
		||||
    mutationFn: async (data: TInput) => {
 | 
			
		||||
      const response = await axios.request({
 | 
			
		||||
        url,
 | 
			
		||||
        method,
 | 
			
		||||
        data,
 | 
			
		||||
      })
 | 
			
		||||
      return response.data
 | 
			
		||||
    },
 | 
			
		||||
    onSuccess: (data) => {
 | 
			
		||||
      invalidate.forEach((key) =>
 | 
			
		||||
        queryClient.invalidateQueries({ queryKey: key })
 | 
			
		||||
      )
 | 
			
		||||
      onSuccess?.(data)
 | 
			
		||||
    },
 | 
			
		||||
    onError,
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										26
									
								
								src/hooks/useQueryData.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/hooks/useQueryData.ts
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,26 @@
 | 
			
		|||
import { useQuery } from '@tanstack/react-query';
 | 
			
		||||
import axios from 'axios';
 | 
			
		||||
 | 
			
		||||
type QueryDataOptions<T> = {
 | 
			
		||||
    queryKey: string[];
 | 
			
		||||
    url: string;
 | 
			
		||||
    params?: Record<string, any>;
 | 
			
		||||
    select?: (data: any) => T;
 | 
			
		||||
    enabled?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function useQueryData<T = any>({
 | 
			
		||||
  queryKey,
 | 
			
		||||
  url,
 | 
			
		||||
  params,
 | 
			
		||||
  select,
 | 
			
		||||
  enabled = true,
 | 
			
		||||
}: QueryDataOptions<T>) {
 | 
			
		||||
  return useQuery<T>({
 | 
			
		||||
    queryKey,
 | 
			
		||||
    queryFn: () => axios.get(url, { params }).then((res) => res.data),
 | 
			
		||||
    select,
 | 
			
		||||
    enabled,
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										101
									
								
								src/layouts/app-layout.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								src/layouts/app-layout.tsx
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,101 @@
 | 
			
		|||
import type { ReactNode } from "react";
 | 
			
		||||
import { AppSidebar } from "@/components/app-sidebar";
 | 
			
		||||
import {
 | 
			
		||||
  SidebarProvider,
 | 
			
		||||
  SidebarInset,
 | 
			
		||||
  SidebarTrigger,
 | 
			
		||||
} from "@/components/ui/sidebar";
 | 
			
		||||
import { Home, Building, AppWindow } from "lucide-react";
 | 
			
		||||
import { Toaster } from "@/components/ui/sonner";
 | 
			
		||||
import { useQueryClient } from "@tanstack/react-query";
 | 
			
		||||
import { API_ENDPOINTS, BASE_URL } from "@/config/api";
 | 
			
		||||
import { Separator } from "@/components/ui/separator";
 | 
			
		||||
 | 
			
		||||
type AppLayoutProps = {
 | 
			
		||||
  children: ReactNode;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default function AppLayout({ children }: AppLayoutProps) {
 | 
			
		||||
  const queryClient = useQueryClient();
 | 
			
		||||
 | 
			
		||||
  const handlePrefetchApps = () => {
 | 
			
		||||
    queryClient.prefetchQuery({
 | 
			
		||||
      queryKey: ["app-version"],
 | 
			
		||||
      queryFn: () =>
 | 
			
		||||
        fetch(BASE_URL + API_ENDPOINTS.APP_VERSION.GET_VERSION).then((res) =>
 | 
			
		||||
          res.json()
 | 
			
		||||
        ),
 | 
			
		||||
      staleTime: 60 * 1000,
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handlePrefetchRooms = () => {
 | 
			
		||||
    queryClient.prefetchQuery({
 | 
			
		||||
      queryKey: ["room-list"],
 | 
			
		||||
      queryFn: () =>
 | 
			
		||||
        fetch(BASE_URL + API_ENDPOINTS.DEVICE_COMM.GET_ROOM_LIST).then((res) =>
 | 
			
		||||
          res.json()
 | 
			
		||||
        ),
 | 
			
		||||
      staleTime: 60 * 1000,
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const items = [
 | 
			
		||||
    { title: "Dashboard", to: "/", icon: Home },
 | 
			
		||||
    {
 | 
			
		||||
      title: "Danh sách phòng",
 | 
			
		||||
      to: "/room",
 | 
			
		||||
      icon: Building,
 | 
			
		||||
      onPointerEnter: handlePrefetchRooms,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: "Quản lý Agent",
 | 
			
		||||
      to: "/apps",
 | 
			
		||||
      icon: AppWindow,
 | 
			
		||||
      onPointerEnter: handlePrefetchApps,
 | 
			
		||||
    },
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <SidebarProvider>
 | 
			
		||||
      <div className="flex min-h-screen w-full">
 | 
			
		||||
        <AppSidebar items={items} />
 | 
			
		||||
        <SidebarInset className="flex-1">
 | 
			
		||||
          {/* Mobile header with sidebar trigger */}
 | 
			
		||||
          <header className="flex h-14 shrink-0 items-center gap-2 border-b px-4 lg:hidden">
 | 
			
		||||
            <SidebarTrigger className="-ml-1" />
 | 
			
		||||
            <Separator orientation="vertical" className="mr-2 h-4" />
 | 
			
		||||
            <div className="flex items-center gap-2">
 | 
			
		||||
              <div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
 | 
			
		||||
                <Building className="size-4" />
 | 
			
		||||
              </div>
 | 
			
		||||
              <div className="flex flex-col gap-0.5 leading-none">
 | 
			
		||||
                <span className="font-semibold text-sm">
 | 
			
		||||
                  TTMT Computer Management
 | 
			
		||||
                </span>
 | 
			
		||||
                <span className="text-xs text-muted-foreground">v1.0.0</span>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </header>
 | 
			
		||||
 | 
			
		||||
          {/* Main content with responsive padding */}
 | 
			
		||||
          <main className="flex-1 p-4 sm:p-6 lg:p-8 overflow-auto">
 | 
			
		||||
            {children}
 | 
			
		||||
          </main>
 | 
			
		||||
 | 
			
		||||
          <Toaster
 | 
			
		||||
            position="top-right"
 | 
			
		||||
            toastOptions={{
 | 
			
		||||
              classNames: {
 | 
			
		||||
                toast:
 | 
			
		||||
                  "text-sm sm:text-lg px-4 sm:px-6 py-3 sm:py-4 rounded-xl max-w-sm sm:max-w-md shadow-lg",
 | 
			
		||||
                title: "text-sm sm:text-lg font-semibold",
 | 
			
		||||
                description: "text-xs sm:text-base",
 | 
			
		||||
              },
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
        </SidebarInset>
 | 
			
		||||
      </div>
 | 
			
		||||
    </SidebarProvider>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										6
									
								
								src/lib/utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/lib/utils.ts
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,6 @@
 | 
			
		|||
import { clsx, type ClassValue } from "clsx"
 | 
			
		||||
import { twMerge } from "tailwind-merge"
 | 
			
		||||
 | 
			
		||||
export function cn(...inputs: ClassValue[]) {
 | 
			
		||||
  return twMerge(clsx(inputs))
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										44
									
								
								src/logo.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								src/logo.svg
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,44 @@
 | 
			
		|||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<svg id="Layer_1"
 | 
			
		||||
  xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 841.9 595.3">
 | 
			
		||||
  <!-- Generator: Adobe Illustrator 29.3.0, SVG Export Plug-In . SVG Version: 2.1.0 Build 146)  -->
 | 
			
		||||
  <defs>
 | 
			
		||||
    <style>
 | 
			
		||||
      .st0 {
 | 
			
		||||
        fill: #9ae7fc;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .st1 {
 | 
			
		||||
        fill: #61dafb;
 | 
			
		||||
      }
 | 
			
		||||
    </style>
 | 
			
		||||
  </defs>
 | 
			
		||||
  <g>
 | 
			
		||||
    <path class="st1" d="M666.3,296.5c0-32.5-40.7-63.3-103.1-82.4,14.4-63.6,8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6,0,8.3.9,11.4,2.6,13.6,7.8,19.5,37.5,14.9,75.7-1.1,9.4-2.9,19.3-5.1,29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50,32.6-30.3,63.2-46.9,84-46.9v-22.3c-27.5,0-63.5,19.6-99.9,53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7,0,51.4,16.5,84,46.6-14,14.7-28,31.4-41.3,49.9-22.6,2.4-44,6.1-63.6,11-2.3-10-4-19.7-5.2-29-4.7-38.2,1.1-67.9,14.6-75.8,3-1.8,6.9-2.6,11.5-2.6v-22.3c-8.4,0-16,1.8-22.6,5.6-28.1,16.2-34.4,66.7-19.9,130.1-62.2,19.2-102.7,49.9-102.7,82.3s40.7,63.3,103.1,82.4c-14.4,63.6-8,114.2,20.2,130.4,6.5,3.8,14.1,5.6,22.5,5.6,27.5,0,63.5-19.6,99.9-53.6,36.4,33.8,72.4,53.2,99.9,53.2,8.4,0,16-1.8,22.6-5.6,28.1-16.2,34.4-66.7,19.9-130.1,62-19.1,102.5-49.9,102.5-82.3zm-130.2-66.7c-3.7,12.9-8.3,26.2-13.5,39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4,14.2,2.1,27.9,4.7,41,7.9zm-45.8,106.5c-7.8,13.5-15.8,26.3-24.1,38.2-14.9,1.3-30,2-45.2,2s-30.2-.7-45-1.9c-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8,6.2-13.4,13.2-26.8,20.7-39.9,7.8-13.5,15.8-26.3,24.1-38.2,14.9-1.3,30-2,45.2-2s30.2.7,45,1.9c8.3,11.9,16.4,24.6,24.2,38,7.6,13.1,14.5,26.4,20.8,39.8-6.3,13.4-13.2,26.8-20.7,39.9zm32.3-13c5.4,13.4,10,26.8,13.8,39.8-13.1,3.2-26.9,5.9-41.2,8,4.9-7.7,9.8-15.6,14.4-23.7,4.6-8,8.9-16.1,13-24.1zm-101.4,106.7c-9.3-9.6-18.6-20.3-27.8-32,9,.4,18.2.7,27.5.7s18.7-.2,27.8-.7c-9,11.7-18.3,22.4-27.5,32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9,3.7-12.9,8.3-26.2,13.5-39.5,4.1,8,8.4,16,13.1,24s9.5,15.8,14.4,23.4zm73.9-208.1c9.3,9.6,18.6,20.3,27.8,32-9-.4-18.2-.7-27.5-.7s-18.7.2-27.8.7c9-11.7,18.3-22.4,27.5-32zm-74,58.9c-4.9,7.7-9.8,15.6-14.4,23.7-4.6,8-8.9,16-13,24-5.4-13.4-10-26.8-13.8-39.8,13.1-3.1,26.9-5.8,41.2-7.9zm-90.5,125.2c-35.4-15.1-58.3-34.9-58.3-50.6s22.9-35.6,58.3-50.6c8.6-3.7,18-7,27.7-10.1,5.7,19.6,13.2,40,22.5,60.9-9.2,20.8-16.6,41.1-22.2,60.6-9.9-3.1-19.3-6.5-28-10.2zm53.8,142.9c-13.6-7.8-19.5-37.5-14.9-75.7,1.1-9.4,2.9-19.3,5.1-29.4,19.6,4.8,41,8.5,63.5,10.9,13.5,18.5,27.5,35.3,41.6,50-32.6,30.3-63.2,46.9-84,46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7,38.2-1.1,67.9-14.6,75.8-3,1.8-6.9,2.6-11.5,2.6-20.7,0-51.4-16.5-84-46.6,14-14.7,28-31.4,41.3-49.9,22.6-2.4,44-6.1,63.6-11,2.3,10.1,4.1,19.8,5.2,29.1zm38.5-66.7c-8.6,3.7-18,7-27.7,10.1-5.7-19.6-13.2-40-22.5-60.9,9.2-20.8,16.6-41.1,22.2-60.6,9.9,3.1,19.3,6.5,28.1,10.2,35.4,15.1,58.3,34.9,58.3,50.6,0,15.7-23,35.6-58.4,50.6zm-264.9-268.7z"/>
 | 
			
		||||
    <circle class="st1" cx="420.9" cy="296.5" r="45.7"/>
 | 
			
		||||
    <path class="st1" d="M520.5,78.1"/>
 | 
			
		||||
  </g>
 | 
			
		||||
  <circle class="st0" cx="420.8" cy="296.6" r="43"/>
 | 
			
		||||
  <path class="st1" d="M466.1,296.6c0,25-20.2,45.2-45.2,45.2s-45.2-20.2-45.2-45.2,20.2-45.2,45.2-45.2,45.2,20.2,45.2,45.2ZM386,295.6v-6.3c0-1.1,1.2-5.1,1.8-6.2,1-1.9,2.9-3.5,4.6-4.7l-3.4-3.4c4-3.6,9.4-3.7,13.7-.7,1.9-4.7,6.6-7.1,11.6-6.7l-.8,4.2c5.9.2,13.1,4.1,13.1,10.8s0,.5-.7.7c-1.7.3-3.4-.4-5-.6s-1.2-.4-1.2.3,2.5,4.1,3,5.5,1,3.5.8,5.3c-5.6-.8-10.5-3.2-14.8-6.7.3,2.6,4.1,21.7,5.3,21.9s.8-.6,1-1.1,1.3-6.3,1.3-6.7c0-1-1.7-1.8-2.2-2.8-1.2-2.7,1.3-4.7,3.7-3.3s5.2,6.2,7.5,7.3,13,1.4,14.8,3.3-2.9,4.6-1.5,7.6c6.7-2.6,13.5-3.3,20.6-2.5,3.1-9.7,3.1-20.3-.9-29.8-7.3,0-14.7-3.6-17.2-10.8-2.5-7.2-.7-8.6-1.3-9.3-.8-1-6.3.6-7.4-1.5s.3-1.1-.2-1.4-1.9-.6-2.6-.8c-26-6.4-51.3,15.7-49.7,42.1,0,1.6,1.6,10.3,2.4,11.1s4.8,0,6.3,0,3.7.3,5,.5c2.9.4,7.2,2.4,9.4,2.5s2.4-.8,2.7-2.4c.4-2.6.5-7.4.5-10.1s-1-7.8-1.3-11.6c-.9-.2-.7,0-.9.5-.7,1.3-1.1,3.2-1.9,4.8s-5.2,8.7-5.7,9-.7-.5-.8-.8c-1.6-3.5-2-7.9-1.9-11.8-.9-1-5.4,4.9-6.7,5.3l-.8-.4v-.3h-.2ZM455.6,276.4c1.1-1.2-6-8.9-7.2-10-3-2.7-5.4-4.5-3.5,1.4s5.7,7.8,10.6,8.5h.1ZM410.9,270.1c-.4-.5-6.1,2.9-5.5,4.6,1.9-1.3,5.9-1.7,5.5-4.6ZM400.4,276.4c-.3-2.4-6.3-2.7-7.2-1s1.6,1.4,1.9,1.4c1.8.3,3.5-.6,5.2-.4h.1ZM411.3,276.8c3.8,1.3,6.6,3.6,10.9,3.7s0-3-1.2-3.9c-2.2-1.7-5.1-2.4-7.8-2.4s-1.6-.3-1.4.4c2.8.6,7.3.7,8.4,3.8-2.3-.3-3.9-1.6-6.2-2s-2.5-.5-2.6.3h0ZM420.6,290.3c-.8-5.1-5.7-10.8-10.9-11.6s-1.3-.4-.8.5,4.7,3.2,5.7,4,4.5,4.2,2.1,3.8-8.4-7.8-9.4-6.7c.2.9,1.1,1.9,1.7,2.7,3,3.8,6.9,6.8,11.8,7.4h-.2ZM395.3,279.8c-5,1.1-6.9,6.3-6.7,11,.7.8,5-3.8,5.4-4.5s2.7-4.6,1.1-4-2.9,4.4-4.2,4.6.2-2.1.4-2.5c1.1-1.6,2.9-3.1,4-4.6h0ZM400.4,281.5c-.4-.5-2,1.3-2.3,1.7-2.9,3.9-2.6,10.2-1.5,14.8.8.2.8-.3,1.2-.7,3-3.8,5.5-10.5,4.5-15.4-2.1,3.1-3.1,7.3-3.6,11h-1.3c0-4,1.9-7.7,3-11.4h0ZM426.9,305.9c0-1.7-1.7-1.4-2.5-1.9s-1.3-1.9-3-1.4c1.3,2.1,3,3.2,5.5,3.4h0ZM417.2,308.5c7.6.7,5.5-1.9,1.4-5.5-1.3-.3-1.5,4.5-1.4,5.5ZM437,309.7c-3.5-.3-7.8-2-11.2-2.1s-1.3,0-1.9.7c4,1.3,8.4,1.7,12.1,4l1-2.5h0ZM420.5,312.8c-7.3,0-15.1,3.7-20.4,8.8s-4.8,5.3-4.8,6.2c0,1.8,8.6,6.2,10.5,6.8,12.1,4.8,27.5,3.5,38.2-4.2s3.1-2.7,0-6.2c-5.7-6.6-14.7-11.4-23.4-11.3h-.1ZM398.7,316.9c-1.4-1.4-5-1.9-7-2.1s-5.3-.3-6.9.6l13.9,1.4h0ZM456.9,314.8h-7.4c-.9,0-4.9,1.1-6,1.6s-.8.6,0,.5c2.4,0,5.1-1,7.6-1.3s3.5.2,5.1,0,1.3-.3.6-.8h0Z"/>
 | 
			
		||||
  <path class="st0" d="M386,295.6l.8.4c1.3-.3,5.8-6.2,6.7-5.3,0,3.9.3,8.3,1.9,11.8s0,1.2.8.8,5.1-7.8,5.7-9,1.3-3.5,1.9-4.8,0-.7.9-.5c.3,3.8,1.2,7.8,1.3,11.6s0,7.5-.5,10.1-1.1,2.4-2.7,2.4-6.5-2.1-9.4-2.5-3.7-.5-5-.5-5.4,1.1-6.3,0-2.2-9.5-2.4-11.1c-1.5-26.4,23.7-48.5,49.7-42.1s2.2.4,2.6.8,0,1,.2,1.4c1.1,2,6.5.5,7.4,1.5s.4,6.9,1.3,9.3c2.5,7.2,10,10.9,17.2,10.8,4,9.4,4,20.1.9,29.8-7.2-.7-13.9,0-20.6,2.5-1.3-3.1,4.1-5.1,1.5-7.6s-11.8-1.9-14.8-3.3-5.4-6.1-7.5-7.3-4.9.6-3.7,3.3,2.1,1.8,2.2,2.8-1,6.2-1.3,6.7-.3,1.3-1,1.1c-1.1-.3-5-19.3-5.3-21.9,4.3,3.5,9.2,5.9,14.8,6.7.2-1.9-.3-3.5-.8-5.3s-3-5.1-3-5.5c0-.8.9-.3,1.2-.3,1.6,0,3.3.8,5,.6s.7.3.7-.7c0-6.6-7.2-10.6-13.1-10.8l.8-4.2c-5.1-.3-9.6,2-11.6,6.7-4.3-3-9.8-3-13.7.7l3.4,3.4c-1.8,1.3-3.5,2.8-4.6,4.7s-1.8,5.1-1.8,6.2v6.6h.2ZM431.6,265c7.8,2.1,8.7-3.5.2-1.3l-.2,1.3ZM432.4,270.9c.3.6,6.4-.4,5.8-2.3s-4.6.6-5.7.6l-.2,1.7h.1ZM434.5,276c.8,1.2,5.7-1.8,5.5-2.7-.4-1.9-6.6,1.2-5.5,2.7ZM442.9,276.4c-.9-.9-5,2.8-4.6,4,.6,2.4,5.7-3,4.6-4ZM445.1,279.9c-.3.2-3.1,4.6-1.5,5s3.5-3.4,3.5-4-1.3-1.3-2-.9h0ZM448.9,287.4c2.1.8,3.8-5.1,2.3-5.5-1.9-.6-2.6,5.1-2.3,5.5ZM457.3,288.6c.5-1.7,1.1-4.7-1-5.5-1,.3-.6,3.9-.6,4.8l.3.5,1.3.2h0Z"/>
 | 
			
		||||
  <path class="st0" d="M455.6,276.4c-5-.8-9.1-3.6-10.6-8.5s.5-4,3.5-1.4,8.3,8.7,7.2,10h-.1Z"/>
 | 
			
		||||
  <path class="st0" d="M420.6,290.3c-4.9-.6-8.9-3.6-11.8-7.4s-1.5-1.8-1.7-2.7c1-1,8.5,6.6,9.4,6.7,2.4.4-1.8-3.5-2.1-3.8-1-.8-5.4-3.5-5.7-4-.4-.8.5-.5.8-.5,5.2.8,10.1,6.6,10.9,11.6h.2Z"/>
 | 
			
		||||
  <path class="st0" d="M400.4,281.5c-1.1,3.7-3,7.3-3,11.4h1.3c.5-3.7,1.5-7.8,3.6-11,1,4.8-1.5,11.6-4.5,15.4s-.4.8-1.2.7c-1.1-4.5-1.3-10.8,1.5-14.8s1.9-2.2,2.3-1.7h0Z"/>
 | 
			
		||||
  <path class="st0" d="M411.3,276.8c0-.8,2.1-.4,2.6-.3,2.4.4,4,1.7,6.2,2-1.2-3.1-5.7-3.2-8.4-3.8,0-.8.9-.4,1.4-.4,2.8,0,5.6.7,7.8,2.4,2.2,1.7,4,4,1.2,3.9-4.3,0-7.1-2.4-10.9-3.7h0Z"/>
 | 
			
		||||
  <path class="st0" d="M395.3,279.8c-1.1,1.6-3,3-4,4.6s-1.9,2.8-.4,2.5,2.8-4,4.2-4.6-.9,3.6-1.1,4c-.4.7-4.7,5.2-5.4,4.5-.2-4.6,1.8-9.9,6.7-11h0Z"/>
 | 
			
		||||
  <path class="st0" d="M437,309.7l-1,2.5c-3.6-2.3-8-2.8-12.1-4,.5-.7,1.1-.7,1.9-.7,3.4,0,7.8,1.8,11.2,2.1h0Z"/>
 | 
			
		||||
  <path class="st0" d="M417.2,308.5c0-1,0-5.8,1.4-5.5,4,3.5,6.1,6.2-1.4,5.5Z"/>
 | 
			
		||||
  <path class="st0" d="M400.4,276.4c-1.8-.3-3.5.7-5.2.4s-2.3-.8-1.9-1.4c.8-1.6,6.9-1.4,7.2,1h-.1Z"/>
 | 
			
		||||
  <path class="st0" d="M410.9,270.1c.4,3-3.6,3.3-5.5,4.6-.6-1.8,5-5.1,5.5-4.6Z"/>
 | 
			
		||||
  <path class="st0" d="M426.9,305.9c-2.5-.2-4.1-1.3-5.5-3.4,1.7-.4,2,.8,3,1.4s2.6.3,2.5,1.9h0Z"/>
 | 
			
		||||
  <path class="st1" d="M432.4,270.9l.2-1.7c1.1,0,5.1-2.2,5.7-.6s-5.5,2.9-5.8,2.3h-.1Z"/>
 | 
			
		||||
  <path class="st1" d="M431.6,265l.2-1.3c8.4-2.1,7.7,3.4-.2,1.3Z"/>
 | 
			
		||||
  <path class="st1" d="M434.5,276c-1.1-1.5,5.1-4.6,5.5-2.7s-4.6,4-5.5,2.7Z"/>
 | 
			
		||||
  <path class="st1" d="M442.9,276.4c1.1,1.1-4,6.4-4.6,4s3.7-4.9,4.6-4Z"/>
 | 
			
		||||
  <path class="st1" d="M445.1,279.9c.7-.4,2.1,0,2,.9s-2.4,4.4-3.5,4,1.3-4.8,1.5-5h0Z"/>
 | 
			
		||||
  <path class="st1" d="M448.9,287.4c-.3-.3.4-6.1,2.3-5.5,1.4.4-.2,6.2-2.3,5.5Z"/>
 | 
			
		||||
  <path class="st1" d="M457.3,288.6l-1.3-.2-.3-.5c0-.9-.4-4.6.6-4.8,2.1.8,1.5,3.8,1,5.5h0Z"/>
 | 
			
		||||
  <path class="st0" d="M420.5,312.8c8.9,0,17.9,4.7,23.4,11.3,5.6,6.6,3.8,3.5,0,6.2-10.7,7.7-26.1,9-38.2,4.2-1.9-.8-10.5-5.1-10.5-6.8s4-5.3,4.8-6.2c5.3-5,13.1-8.6,20.4-8.8h.1Z"/>
 | 
			
		||||
  <path class="st0" d="M398.7,316.9l-13.9-1.4c1.7-1,5-.8,6.9-.6s5.6.7,7,2.1h0Z"/>
 | 
			
		||||
  <path class="st0" d="M456.9,314.8c.7.5,0,.8-.6.8-1.6.2-3.5-.2-5.1,0-2.4.3-5.2,1.2-7.6,1.3s-1.1,0,0-.5,5.1-1.6,6-1.6h7.4,0Z"/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 8.4 KiB  | 
							
								
								
									
										44
									
								
								src/main.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								src/main.tsx
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,44 @@
 | 
			
		|||
import { StrictMode } from "react";
 | 
			
		||||
import ReactDOM from "react-dom/client";
 | 
			
		||||
import { RouterProvider, createRouter } from "@tanstack/react-router";
 | 
			
		||||
import useAuthToken from "./hooks/useAuthtoken";
 | 
			
		||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
 | 
			
		||||
// Import the generated route tree
 | 
			
		||||
import { routeTree } from "./routeTree.gen";
 | 
			
		||||
 | 
			
		||||
import "./styles.css";
 | 
			
		||||
 | 
			
		||||
const auth = useAuthToken.getState();
 | 
			
		||||
 | 
			
		||||
const queryClient = new QueryClient();
 | 
			
		||||
 | 
			
		||||
// Create a new router instance
 | 
			
		||||
const router = createRouter({
 | 
			
		||||
  routeTree,
 | 
			
		||||
  context: { auth },
 | 
			
		||||
  defaultPreload: "intent",
 | 
			
		||||
  scrollRestoration: true,
 | 
			
		||||
  defaultStructuralSharing: true,
 | 
			
		||||
  defaultPreloadStaleTime: 0,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Register the router instance for type safety
 | 
			
		||||
declare module "@tanstack/react-router" {
 | 
			
		||||
  interface Register {
 | 
			
		||||
    router: typeof router;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Render the app
 | 
			
		||||
const rootElement = document.getElementById("app");
 | 
			
		||||
if (rootElement && !rootElement.innerHTML) {
 | 
			
		||||
  const root = ReactDOM.createRoot(rootElement);
 | 
			
		||||
  root.render(
 | 
			
		||||
    <StrictMode>
 | 
			
		||||
      <QueryClientProvider client={queryClient}>
 | 
			
		||||
        {" "}
 | 
			
		||||
        <RouterProvider router={router} />
 | 
			
		||||
      </QueryClientProvider>
 | 
			
		||||
    </StrictMode>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										13
									
								
								src/reportWebVitals.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/reportWebVitals.ts
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,13 @@
 | 
			
		|||
const reportWebVitals = (onPerfEntry?: () => void) => {
 | 
			
		||||
  if (onPerfEntry && onPerfEntry instanceof Function) {
 | 
			
		||||
    import('web-vitals').then(({ onCLS, onINP, onFCP, onLCP, onTTFB }) => {
 | 
			
		||||
      onCLS(onPerfEntry)
 | 
			
		||||
      onINP(onPerfEntry)
 | 
			
		||||
      onFCP(onPerfEntry)
 | 
			
		||||
      onLCP(onPerfEntry)
 | 
			
		||||
      onTTFB(onPerfEntry)
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default reportWebVitals
 | 
			
		||||
							
								
								
									
										188
									
								
								src/routeTree.gen.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										188
									
								
								src/routeTree.gen.ts
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,188 @@
 | 
			
		|||
/* eslint-disable */
 | 
			
		||||
 | 
			
		||||
// @ts-nocheck
 | 
			
		||||
 | 
			
		||||
// noinspection JSUnusedGlobalSymbols
 | 
			
		||||
 | 
			
		||||
// This file was automatically generated by TanStack Router.
 | 
			
		||||
// You should NOT make any changes in this file as it will be overwritten.
 | 
			
		||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
 | 
			
		||||
 | 
			
		||||
import { Route as rootRouteImport } from './routes/__root'
 | 
			
		||||
import { Route as AuthenticatedRouteImport } from './routes/_authenticated'
 | 
			
		||||
import { Route as AuthRouteImport } from './routes/_auth'
 | 
			
		||||
import { Route as IndexRouteImport } from './routes/index'
 | 
			
		||||
import { Route as AuthenticatedRoomIndexRouteImport } from './routes/_authenticated/room/index'
 | 
			
		||||
import { Route as AuthenticatedAppsIndexRouteImport } from './routes/_authenticated/apps/index'
 | 
			
		||||
import { Route as AuthLoginIndexRouteImport } from './routes/_auth/login/index'
 | 
			
		||||
import { Route as AuthenticatedRoomRoomNameIndexRouteImport } from './routes/_authenticated/room/$roomName/index'
 | 
			
		||||
 | 
			
		||||
const AuthenticatedRoute = AuthenticatedRouteImport.update({
 | 
			
		||||
  id: '/_authenticated',
 | 
			
		||||
  getParentRoute: () => rootRouteImport,
 | 
			
		||||
} as any)
 | 
			
		||||
const AuthRoute = AuthRouteImport.update({
 | 
			
		||||
  id: '/_auth',
 | 
			
		||||
  getParentRoute: () => rootRouteImport,
 | 
			
		||||
} as any)
 | 
			
		||||
const IndexRoute = IndexRouteImport.update({
 | 
			
		||||
  id: '/',
 | 
			
		||||
  path: '/',
 | 
			
		||||
  getParentRoute: () => rootRouteImport,
 | 
			
		||||
} as any)
 | 
			
		||||
const AuthenticatedRoomIndexRoute = AuthenticatedRoomIndexRouteImport.update({
 | 
			
		||||
  id: '/room/',
 | 
			
		||||
  path: '/room/',
 | 
			
		||||
  getParentRoute: () => AuthenticatedRoute,
 | 
			
		||||
} as any)
 | 
			
		||||
const AuthenticatedAppsIndexRoute = AuthenticatedAppsIndexRouteImport.update({
 | 
			
		||||
  id: '/apps/',
 | 
			
		||||
  path: '/apps/',
 | 
			
		||||
  getParentRoute: () => AuthenticatedRoute,
 | 
			
		||||
} as any)
 | 
			
		||||
const AuthLoginIndexRoute = AuthLoginIndexRouteImport.update({
 | 
			
		||||
  id: '/login/',
 | 
			
		||||
  path: '/login/',
 | 
			
		||||
  getParentRoute: () => AuthRoute,
 | 
			
		||||
} as any)
 | 
			
		||||
const AuthenticatedRoomRoomNameIndexRoute =
 | 
			
		||||
  AuthenticatedRoomRoomNameIndexRouteImport.update({
 | 
			
		||||
    id: '/room/$roomName/',
 | 
			
		||||
    path: '/room/$roomName/',
 | 
			
		||||
    getParentRoute: () => AuthenticatedRoute,
 | 
			
		||||
  } as any)
 | 
			
		||||
 | 
			
		||||
export interface FileRoutesByFullPath {
 | 
			
		||||
  '/': typeof IndexRoute
 | 
			
		||||
  '/login': typeof AuthLoginIndexRoute
 | 
			
		||||
  '/apps': typeof AuthenticatedAppsIndexRoute
 | 
			
		||||
  '/room': typeof AuthenticatedRoomIndexRoute
 | 
			
		||||
  '/room/$roomName': typeof AuthenticatedRoomRoomNameIndexRoute
 | 
			
		||||
}
 | 
			
		||||
export interface FileRoutesByTo {
 | 
			
		||||
  '/': typeof IndexRoute
 | 
			
		||||
  '/login': typeof AuthLoginIndexRoute
 | 
			
		||||
  '/apps': typeof AuthenticatedAppsIndexRoute
 | 
			
		||||
  '/room': typeof AuthenticatedRoomIndexRoute
 | 
			
		||||
  '/room/$roomName': typeof AuthenticatedRoomRoomNameIndexRoute
 | 
			
		||||
}
 | 
			
		||||
export interface FileRoutesById {
 | 
			
		||||
  __root__: typeof rootRouteImport
 | 
			
		||||
  '/': typeof IndexRoute
 | 
			
		||||
  '/_auth': typeof AuthRouteWithChildren
 | 
			
		||||
  '/_authenticated': typeof AuthenticatedRouteWithChildren
 | 
			
		||||
  '/_auth/login/': typeof AuthLoginIndexRoute
 | 
			
		||||
  '/_authenticated/apps/': typeof AuthenticatedAppsIndexRoute
 | 
			
		||||
  '/_authenticated/room/': typeof AuthenticatedRoomIndexRoute
 | 
			
		||||
  '/_authenticated/room/$roomName/': typeof AuthenticatedRoomRoomNameIndexRoute
 | 
			
		||||
}
 | 
			
		||||
export interface FileRouteTypes {
 | 
			
		||||
  fileRoutesByFullPath: FileRoutesByFullPath
 | 
			
		||||
  fullPaths: '/' | '/login' | '/apps' | '/room' | '/room/$roomName'
 | 
			
		||||
  fileRoutesByTo: FileRoutesByTo
 | 
			
		||||
  to: '/' | '/login' | '/apps' | '/room' | '/room/$roomName'
 | 
			
		||||
  id:
 | 
			
		||||
    | '__root__'
 | 
			
		||||
    | '/'
 | 
			
		||||
    | '/_auth'
 | 
			
		||||
    | '/_authenticated'
 | 
			
		||||
    | '/_auth/login/'
 | 
			
		||||
    | '/_authenticated/apps/'
 | 
			
		||||
    | '/_authenticated/room/'
 | 
			
		||||
    | '/_authenticated/room/$roomName/'
 | 
			
		||||
  fileRoutesById: FileRoutesById
 | 
			
		||||
}
 | 
			
		||||
export interface RootRouteChildren {
 | 
			
		||||
  IndexRoute: typeof IndexRoute
 | 
			
		||||
  AuthRoute: typeof AuthRouteWithChildren
 | 
			
		||||
  AuthenticatedRoute: typeof AuthenticatedRouteWithChildren
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
declare module '@tanstack/react-router' {
 | 
			
		||||
  interface FileRoutesByPath {
 | 
			
		||||
    '/_authenticated': {
 | 
			
		||||
      id: '/_authenticated'
 | 
			
		||||
      path: ''
 | 
			
		||||
      fullPath: ''
 | 
			
		||||
      preLoaderRoute: typeof AuthenticatedRouteImport
 | 
			
		||||
      parentRoute: typeof rootRouteImport
 | 
			
		||||
    }
 | 
			
		||||
    '/_auth': {
 | 
			
		||||
      id: '/_auth'
 | 
			
		||||
      path: ''
 | 
			
		||||
      fullPath: ''
 | 
			
		||||
      preLoaderRoute: typeof AuthRouteImport
 | 
			
		||||
      parentRoute: typeof rootRouteImport
 | 
			
		||||
    }
 | 
			
		||||
    '/': {
 | 
			
		||||
      id: '/'
 | 
			
		||||
      path: '/'
 | 
			
		||||
      fullPath: '/'
 | 
			
		||||
      preLoaderRoute: typeof IndexRouteImport
 | 
			
		||||
      parentRoute: typeof rootRouteImport
 | 
			
		||||
    }
 | 
			
		||||
    '/_authenticated/room/': {
 | 
			
		||||
      id: '/_authenticated/room/'
 | 
			
		||||
      path: '/room'
 | 
			
		||||
      fullPath: '/room'
 | 
			
		||||
      preLoaderRoute: typeof AuthenticatedRoomIndexRouteImport
 | 
			
		||||
      parentRoute: typeof AuthenticatedRoute
 | 
			
		||||
    }
 | 
			
		||||
    '/_authenticated/apps/': {
 | 
			
		||||
      id: '/_authenticated/apps/'
 | 
			
		||||
      path: '/apps'
 | 
			
		||||
      fullPath: '/apps'
 | 
			
		||||
      preLoaderRoute: typeof AuthenticatedAppsIndexRouteImport
 | 
			
		||||
      parentRoute: typeof AuthenticatedRoute
 | 
			
		||||
    }
 | 
			
		||||
    '/_auth/login/': {
 | 
			
		||||
      id: '/_auth/login/'
 | 
			
		||||
      path: '/login'
 | 
			
		||||
      fullPath: '/login'
 | 
			
		||||
      preLoaderRoute: typeof AuthLoginIndexRouteImport
 | 
			
		||||
      parentRoute: typeof AuthRoute
 | 
			
		||||
    }
 | 
			
		||||
    '/_authenticated/room/$roomName/': {
 | 
			
		||||
      id: '/_authenticated/room/$roomName/'
 | 
			
		||||
      path: '/room/$roomName'
 | 
			
		||||
      fullPath: '/room/$roomName'
 | 
			
		||||
      preLoaderRoute: typeof AuthenticatedRoomRoomNameIndexRouteImport
 | 
			
		||||
      parentRoute: typeof AuthenticatedRoute
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface AuthRouteChildren {
 | 
			
		||||
  AuthLoginIndexRoute: typeof AuthLoginIndexRoute
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const AuthRouteChildren: AuthRouteChildren = {
 | 
			
		||||
  AuthLoginIndexRoute: AuthLoginIndexRoute,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const AuthRouteWithChildren = AuthRoute._addFileChildren(AuthRouteChildren)
 | 
			
		||||
 | 
			
		||||
interface AuthenticatedRouteChildren {
 | 
			
		||||
  AuthenticatedAppsIndexRoute: typeof AuthenticatedAppsIndexRoute
 | 
			
		||||
  AuthenticatedRoomIndexRoute: typeof AuthenticatedRoomIndexRoute
 | 
			
		||||
  AuthenticatedRoomRoomNameIndexRoute: typeof AuthenticatedRoomRoomNameIndexRoute
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const AuthenticatedRouteChildren: AuthenticatedRouteChildren = {
 | 
			
		||||
  AuthenticatedAppsIndexRoute: AuthenticatedAppsIndexRoute,
 | 
			
		||||
  AuthenticatedRoomIndexRoute: AuthenticatedRoomIndexRoute,
 | 
			
		||||
  AuthenticatedRoomRoomNameIndexRoute: AuthenticatedRoomRoomNameIndexRoute,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const AuthenticatedRouteWithChildren = AuthenticatedRoute._addFileChildren(
 | 
			
		||||
  AuthenticatedRouteChildren,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const rootRouteChildren: RootRouteChildren = {
 | 
			
		||||
  IndexRoute: IndexRoute,
 | 
			
		||||
  AuthRoute: AuthRouteWithChildren,
 | 
			
		||||
  AuthenticatedRoute: AuthenticatedRouteWithChildren,
 | 
			
		||||
}
 | 
			
		||||
export const routeTree = rootRouteImport
 | 
			
		||||
  ._addFileChildren(rootRouteChildren)
 | 
			
		||||
  ._addFileTypes<FileRouteTypes>()
 | 
			
		||||
							
								
								
									
										15
									
								
								src/routes/__root.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/routes/__root.tsx
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,15 @@
 | 
			
		|||
import { Outlet, createRootRouteWithContext, HeadContent } from '@tanstack/react-router'
 | 
			
		||||
import type { AuthTokenProps } from '@/hooks/useAuthtoken'
 | 
			
		||||
 | 
			
		||||
export interface RouterContext {
 | 
			
		||||
  auth: AuthTokenProps
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const Route = createRootRouteWithContext<RouterContext>()({
 | 
			
		||||
  component: () => (
 | 
			
		||||
    <>
 | 
			
		||||
      <HeadContent />
 | 
			
		||||
      <Outlet />
 | 
			
		||||
    </>
 | 
			
		||||
  ),
 | 
			
		||||
})
 | 
			
		||||
							
								
								
									
										16
									
								
								src/routes/_auth.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/routes/_auth.tsx
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,16 @@
 | 
			
		|||
import {createFileRoute, Outlet, redirect} from '@tanstack/react-router'
 | 
			
		||||
 | 
			
		||||
export const Route = createFileRoute('/_auth')({
 | 
			
		||||
  beforeLoad: async ({context}) => {
 | 
			
		||||
    const {authToken} = context.auth
 | 
			
		||||
    if (authToken) {
 | 
			
		||||
      throw redirect({to: '/'})
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  component:AuthLayout ,
 | 
			
		||||
})
 | 
			
		||||
function AuthLayout() {
 | 
			
		||||
  return (
 | 
			
		||||
      <Outlet />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										125
									
								
								src/routes/_auth/login/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								src/routes/_auth/login/index.tsx
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,125 @@
 | 
			
		|||
import { createFileRoute, redirect } from '@tanstack/react-router'
 | 
			
		||||
import {
 | 
			
		||||
  Card,
 | 
			
		||||
  CardContent,
 | 
			
		||||
  CardDescription,
 | 
			
		||||
  CardFooter,
 | 
			
		||||
  CardHeader,
 | 
			
		||||
  CardTitle,
 | 
			
		||||
} from '@/components/ui/card'
 | 
			
		||||
import { Input } from '@/components/ui/input'
 | 
			
		||||
import { Label } from '@/components/ui/label'
 | 
			
		||||
import { Button } from '@/components/ui/button'
 | 
			
		||||
import {
 | 
			
		||||
  formOptions,
 | 
			
		||||
  useForm,
 | 
			
		||||
} from '@tanstack/react-form'
 | 
			
		||||
 | 
			
		||||
interface LoginFormProps {
 | 
			
		||||
  username: string
 | 
			
		||||
  password: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const defaultInput: LoginFormProps = {
 | 
			
		||||
  username: '',
 | 
			
		||||
  password: '',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const formOpts = formOptions({
 | 
			
		||||
  defaultValues: defaultInput,
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const Route = createFileRoute('/_auth/login/')({
 | 
			
		||||
  beforeLoad: async ({ context }) => {
 | 
			
		||||
    const { authToken } = context.auth
 | 
			
		||||
    if (authToken) throw redirect({ to: '/' })
 | 
			
		||||
  },
 | 
			
		||||
  component: LoginForm,
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
function LoginForm() {
 | 
			
		||||
  const form = useForm({
 | 
			
		||||
    ...formOpts,
 | 
			
		||||
    onSubmit: async ({ value }) => {
 | 
			
		||||
      console.log('Submitting login form with values:', value)
 | 
			
		||||
 | 
			
		||||
      // Giả lập đăng nhập
 | 
			
		||||
      if (value.username === 'admin' && value.password === '123456') {
 | 
			
		||||
        alert('Đăng nhập thành công!')
 | 
			
		||||
        // Thêm xử lý lưu token, redirect...
 | 
			
		||||
      } else {
 | 
			
		||||
        alert('Tài khoản hoặc mật khẩu không đúng.')
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Card className="max-w-md mx-auto mt-20 p-6">
 | 
			
		||||
      <CardHeader>
 | 
			
		||||
        <CardTitle>Đăng nhập</CardTitle>
 | 
			
		||||
        <CardDescription>
 | 
			
		||||
          Vui lòng nhập thông tin đăng nhập của bạn.
 | 
			
		||||
        </CardDescription>
 | 
			
		||||
      </CardHeader>
 | 
			
		||||
      <CardContent>
 | 
			
		||||
        <form
 | 
			
		||||
          onSubmit={(e) => {
 | 
			
		||||
            e.preventDefault()
 | 
			
		||||
            form.handleSubmit()
 | 
			
		||||
          }}
 | 
			
		||||
          className="space-y-4"
 | 
			
		||||
        >
 | 
			
		||||
          {/* Username */}
 | 
			
		||||
          <form.Field name="username">
 | 
			
		||||
            {(field) => (
 | 
			
		||||
              <div>
 | 
			
		||||
                <Label htmlFor="username">Tên đăng nhập</Label>
 | 
			
		||||
                <Input
 | 
			
		||||
                  id="username"
 | 
			
		||||
                  value={field.state.value}
 | 
			
		||||
                  onChange={(e) => field.handleChange(e.target.value)}
 | 
			
		||||
                  placeholder="Tên đăng nhập"
 | 
			
		||||
                />
 | 
			
		||||
                {field.state.meta.isTouched && field.state.meta.errors && (
 | 
			
		||||
                  <p className="text-sm text-red-500 mt-1">
 | 
			
		||||
                    {field.state.meta.errors}
 | 
			
		||||
                  </p>
 | 
			
		||||
                )}
 | 
			
		||||
              </div>
 | 
			
		||||
            )}
 | 
			
		||||
          </form.Field>
 | 
			
		||||
 | 
			
		||||
          {/* Password */}
 | 
			
		||||
          <form.Field name="password">
 | 
			
		||||
            {(field) => (
 | 
			
		||||
              <div>
 | 
			
		||||
                <Label htmlFor="password">Mật khẩu</Label>
 | 
			
		||||
                <Input
 | 
			
		||||
                  id="password"
 | 
			
		||||
                  type="password"
 | 
			
		||||
                  value={field.state.value}
 | 
			
		||||
                  onChange={(e) => field.handleChange(e.target.value)}
 | 
			
		||||
                  placeholder="Mật khẩu"
 | 
			
		||||
                />
 | 
			
		||||
                {field.state.meta.isTouched && field.state.meta.errors && (
 | 
			
		||||
                  <p className="text-sm text-red-500 mt-1">
 | 
			
		||||
                    {field.state.meta.errors}
 | 
			
		||||
                  </p>
 | 
			
		||||
                )}
 | 
			
		||||
              </div>
 | 
			
		||||
            )}
 | 
			
		||||
          </form.Field>
 | 
			
		||||
 | 
			
		||||
          <Button type="submit" className="w-full">
 | 
			
		||||
            Đăng nhập
 | 
			
		||||
          </Button>
 | 
			
		||||
        </form>
 | 
			
		||||
      </CardContent>
 | 
			
		||||
      <CardFooter>
 | 
			
		||||
        <p className="text-sm text-muted-foreground">
 | 
			
		||||
          Chưa có tài khoản? <span className="underline cursor-pointer">Đăng ký</span>
 | 
			
		||||
        </p>
 | 
			
		||||
      </CardFooter>
 | 
			
		||||
    </Card>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										21
									
								
								src/routes/_authenticated.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/routes/_authenticated.tsx
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,21 @@
 | 
			
		|||
import { createFileRoute, Outlet, redirect } from '@tanstack/react-router'
 | 
			
		||||
import AppLayout from '@/layouts/app-layout'
 | 
			
		||||
 | 
			
		||||
export const Route = createFileRoute('/_authenticated')({
 | 
			
		||||
  // Kiểm tra auth trước khi render
 | 
			
		||||
  // beforeLoad: async ({context}) => {
 | 
			
		||||
  //     const {authToken} = context.auth
 | 
			
		||||
  //     if (!authToken) {
 | 
			
		||||
  //       throw redirect({to: '/login'})
 | 
			
		||||
  //     }
 | 
			
		||||
  //   },
 | 
			
		||||
  component: AuthenticatedLayout,
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
function AuthenticatedLayout() {
 | 
			
		||||
  return (
 | 
			
		||||
    <AppLayout>
 | 
			
		||||
      <Outlet />
 | 
			
		||||
    </AppLayout>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										246
									
								
								src/routes/_authenticated/apps/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										246
									
								
								src/routes/_authenticated/apps/index.tsx
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,246 @@
 | 
			
		|||
import { createFileRoute } from "@tanstack/react-router";
 | 
			
		||||
import {
 | 
			
		||||
  Card,
 | 
			
		||||
  CardContent,
 | 
			
		||||
  CardDescription,
 | 
			
		||||
  CardFooter,
 | 
			
		||||
  CardHeader,
 | 
			
		||||
  CardTitle,
 | 
			
		||||
} from "@/components/ui/card";
 | 
			
		||||
import { Input } from "@/components/ui/input";
 | 
			
		||||
import { Button } from "@/components/ui/button";
 | 
			
		||||
import { useQueryData } from "@/hooks/useQueryData";
 | 
			
		||||
import { useMutationData } from "@/hooks/useMutationData";
 | 
			
		||||
import { formOptions, useForm } from "@tanstack/react-form";
 | 
			
		||||
import { toast } from "sonner";
 | 
			
		||||
import { Label } from "@/components/ui/label";
 | 
			
		||||
import { useState } from "react";
 | 
			
		||||
import {
 | 
			
		||||
  Dialog,
 | 
			
		||||
  DialogContent,
 | 
			
		||||
  DialogDescription,
 | 
			
		||||
  DialogFooter,
 | 
			
		||||
  DialogHeader,
 | 
			
		||||
  DialogTitle,
 | 
			
		||||
  DialogTrigger,
 | 
			
		||||
} from "@/components/ui/dialog";
 | 
			
		||||
import { FileText, Plus } from "lucide-react";
 | 
			
		||||
import {
 | 
			
		||||
  Table,
 | 
			
		||||
  TableBody,
 | 
			
		||||
  TableCell,
 | 
			
		||||
  TableHead,
 | 
			
		||||
  TableHeader,
 | 
			
		||||
  TableRow,
 | 
			
		||||
} from "@/components/ui/table";
 | 
			
		||||
 | 
			
		||||
import { BASE_URL, API_ENDPOINTS } from "@/config/api";
 | 
			
		||||
 | 
			
		||||
interface UploadAppFormProps {
 | 
			
		||||
  files: FileList;
 | 
			
		||||
  newVersion: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const defaultInput: UploadAppFormProps = {
 | 
			
		||||
  files: new DataTransfer().files,
 | 
			
		||||
  newVersion: "",
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const formOpts = formOptions({
 | 
			
		||||
  defaultValues: defaultInput,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const Route = createFileRoute("/_authenticated/apps/")({
 | 
			
		||||
  head: () => ({
 | 
			
		||||
    meta: [{ title: "Quản lý Agent" }],
 | 
			
		||||
  }),
 | 
			
		||||
  component: AppsComponent,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function AppsComponent() {
 | 
			
		||||
  const { data: versionData, isLoading } = useQueryData({
 | 
			
		||||
    queryKey: ["app-version"],
 | 
			
		||||
    url: BASE_URL + API_ENDPOINTS.APP_VERSION.GET_VERSION,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const versionList = Array.isArray(versionData)
 | 
			
		||||
    ? versionData
 | 
			
		||||
    : versionData
 | 
			
		||||
      ? [versionData]
 | 
			
		||||
      : [];
 | 
			
		||||
 | 
			
		||||
  const [isUploadOpen, setIsUploadOpen] = useState(false);
 | 
			
		||||
 | 
			
		||||
  const uploadMutation = useMutationData<FormData>({
 | 
			
		||||
    url: BASE_URL + API_ENDPOINTS.APP_VERSION.UPLOAD,
 | 
			
		||||
    method: "POST",
 | 
			
		||||
    onSuccess: () => {
 | 
			
		||||
      toast.success("Cập nhật thành công!");
 | 
			
		||||
      setIsUploadOpen(false);
 | 
			
		||||
      form.reset();
 | 
			
		||||
    },
 | 
			
		||||
    onError: () => toast.error("Lỗi khi cập nhật phiên bản!"),
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const updateAgentMutation = useMutationData<void>({
 | 
			
		||||
    url: BASE_URL + API_ENDPOINTS.DEVICE_COMM.UPDATE_AGENT,
 | 
			
		||||
    method: "POST",
 | 
			
		||||
    onSuccess: () => toast.success("Đã gửi yêu cầu cập nhật đến thiết bị!"),
 | 
			
		||||
    onError: () => toast.error("Gửi yêu cầu thất bại!"),
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const form = useForm({
 | 
			
		||||
    ...formOpts,
 | 
			
		||||
    onSubmit: async ({ value }) => {
 | 
			
		||||
      const typedValue = value as UploadAppFormProps;
 | 
			
		||||
      if (!typedValue.newVersion || typedValue.files.length === 0) {
 | 
			
		||||
        toast.error("Vui lòng điền đầy đủ thông tin");
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      const formData = new FormData();
 | 
			
		||||
      Array.from(typedValue.files).forEach((file) =>
 | 
			
		||||
        formData.append("files", file)
 | 
			
		||||
      );
 | 
			
		||||
      formData.append("newVersion", typedValue.newVersion);
 | 
			
		||||
      await uploadMutation.mutateAsync(formData);
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="w-full px-6 space-y-4">
 | 
			
		||||
      <div className="flex items-center justify-between">
 | 
			
		||||
        <div>
 | 
			
		||||
          <h1 className="text-3xl font-bold">Quản lý Agent</h1>
 | 
			
		||||
          <p className="text-muted-foreground mt-2">
 | 
			
		||||
            Quản lý và theo dõi các phiên bản Agent
 | 
			
		||||
          </p>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <Dialog open={isUploadOpen} onOpenChange={setIsUploadOpen}>
 | 
			
		||||
          <DialogTrigger asChild>
 | 
			
		||||
            <Button className="gap-2">
 | 
			
		||||
              <Plus className="h-4 w-4" />
 | 
			
		||||
              Cập nhật phiên bản mới
 | 
			
		||||
            </Button>
 | 
			
		||||
          </DialogTrigger>
 | 
			
		||||
          <DialogContent className="sm:max-w-md">
 | 
			
		||||
            <DialogHeader>
 | 
			
		||||
              <DialogTitle>Cập nhật phiên bản mới</DialogTitle>
 | 
			
		||||
              <DialogDescription>
 | 
			
		||||
                Chọn tệp và nhập số phiên bản
 | 
			
		||||
              </DialogDescription>
 | 
			
		||||
            </DialogHeader>
 | 
			
		||||
 | 
			
		||||
            <form
 | 
			
		||||
              className="space-y-4"
 | 
			
		||||
              onSubmit={(e) => {
 | 
			
		||||
                e.preventDefault();
 | 
			
		||||
                form.handleSubmit();
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <form.Field name="newVersion">
 | 
			
		||||
                {(field) => (
 | 
			
		||||
                  <div className="space-y-2">
 | 
			
		||||
                    <Label>Phiên bản</Label>
 | 
			
		||||
                    <Input
 | 
			
		||||
                      value={field.state.value}
 | 
			
		||||
                      onChange={(e) => field.handleChange(e.target.value)}
 | 
			
		||||
                      placeholder="e.g., 1.0.0"
 | 
			
		||||
                    />
 | 
			
		||||
                  </div>
 | 
			
		||||
                )}
 | 
			
		||||
              </form.Field>
 | 
			
		||||
 | 
			
		||||
              <form.Field name="files">
 | 
			
		||||
                {(field) => (
 | 
			
		||||
                  <div className="space-y-2">
 | 
			
		||||
                    <Label>File ứng dụng</Label>
 | 
			
		||||
                    <Input
 | 
			
		||||
                      type="file"
 | 
			
		||||
                      accept=".exe,.zip,.apk"
 | 
			
		||||
                      onChange={(e) => {
 | 
			
		||||
                        if (e.target.files) {
 | 
			
		||||
                          field.handleChange(e.target.files);
 | 
			
		||||
                        }
 | 
			
		||||
                      }}
 | 
			
		||||
                    />
 | 
			
		||||
                  </div>
 | 
			
		||||
                )}
 | 
			
		||||
              </form.Field>
 | 
			
		||||
 | 
			
		||||
              <DialogFooter>
 | 
			
		||||
                <Button
 | 
			
		||||
                  type="button"
 | 
			
		||||
                  variant="outline"
 | 
			
		||||
                  onClick={() => setIsUploadOpen(false)}
 | 
			
		||||
                >
 | 
			
		||||
                  Hủy
 | 
			
		||||
                </Button>
 | 
			
		||||
                <Button type="submit">Tải lên</Button>
 | 
			
		||||
              </DialogFooter>
 | 
			
		||||
            </form>
 | 
			
		||||
          </DialogContent>
 | 
			
		||||
        </Dialog>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <Card className="w-full">
 | 
			
		||||
        <CardHeader>
 | 
			
		||||
          <CardTitle className="flex items-center gap-2">
 | 
			
		||||
            <FileText className="h-5 w-5" />
 | 
			
		||||
            Lịch sử phiên bản
 | 
			
		||||
          </CardTitle>
 | 
			
		||||
          <CardDescription>
 | 
			
		||||
            Tất cả các phiên bản đã tải lên của Agent
 | 
			
		||||
          </CardDescription>
 | 
			
		||||
        </CardHeader>
 | 
			
		||||
        <CardContent>
 | 
			
		||||
          <Table>
 | 
			
		||||
            <TableHeader>
 | 
			
		||||
              <TableRow>
 | 
			
		||||
                <TableHead>Phiên bản</TableHead>
 | 
			
		||||
                <TableHead>Tên tệp</TableHead>
 | 
			
		||||
                <TableHead>Đường dẫn thư mục</TableHead>
 | 
			
		||||
                <TableHead>Thời gian cập nhật</TableHead>
 | 
			
		||||
              </TableRow>
 | 
			
		||||
            </TableHeader>
 | 
			
		||||
            <TableBody>
 | 
			
		||||
              {isLoading ? (
 | 
			
		||||
                <TableRow>
 | 
			
		||||
                  <TableCell colSpan={4}>Đang tải dữ liệu...</TableCell>
 | 
			
		||||
                </TableRow>
 | 
			
		||||
              ) : versionList.length === 0 ? (
 | 
			
		||||
                <TableRow>
 | 
			
		||||
                  <TableCell colSpan={4}>Không có dữ liệu phiên bản.</TableCell>
 | 
			
		||||
                </TableRow>
 | 
			
		||||
              ) : (
 | 
			
		||||
                versionList.map((v: any) => (
 | 
			
		||||
                  <TableRow key={v.id || v.version}>
 | 
			
		||||
                    <TableCell>{v.version}</TableCell>
 | 
			
		||||
                    <TableCell>{v.fileName}</TableCell>
 | 
			
		||||
                    <TableCell>{v.folderPath}</TableCell>
 | 
			
		||||
                    <TableCell>
 | 
			
		||||
                      {v.updatedAt
 | 
			
		||||
                        ? new Date(v.updatedAt).toLocaleString("vi-VN")
 | 
			
		||||
                        : "N/A"}
 | 
			
		||||
                    </TableCell>
 | 
			
		||||
                  </TableRow>
 | 
			
		||||
                ))
 | 
			
		||||
              )}
 | 
			
		||||
            </TableBody>
 | 
			
		||||
          </Table>
 | 
			
		||||
        </CardContent>
 | 
			
		||||
        <CardFooter>
 | 
			
		||||
          <Button
 | 
			
		||||
            variant="outline"
 | 
			
		||||
            onClick={() => updateAgentMutation.mutateAsync()}
 | 
			
		||||
            disabled={updateAgentMutation.isPending}
 | 
			
		||||
          >
 | 
			
		||||
            {updateAgentMutation.isPending
 | 
			
		||||
              ? "Đang gửi..."
 | 
			
		||||
              : "Yêu cầu thiết bị cập nhật"}
 | 
			
		||||
          </Button>
 | 
			
		||||
        </CardFooter>
 | 
			
		||||
      </Card>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										9
									
								
								src/routes/_authenticated/room/$roomName/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/routes/_authenticated/room/$roomName/index.tsx
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,9 @@
 | 
			
		|||
import { createFileRoute } from '@tanstack/react-router'
 | 
			
		||||
 | 
			
		||||
export const Route = createFileRoute('/_authenticated/room/$roomName/')({
 | 
			
		||||
  component: RouteComponent,
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
function RouteComponent() {
 | 
			
		||||
  return <div>Hello "/_authenticated/room/$roomName/"!</div>
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										9
									
								
								src/routes/_authenticated/room/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/routes/_authenticated/room/index.tsx
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,9 @@
 | 
			
		|||
import { createFileRoute } from '@tanstack/react-router'
 | 
			
		||||
 | 
			
		||||
export const Route = createFileRoute('/_authenticated/room/')({
 | 
			
		||||
  component: RoomComponent,
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
function RoomComponent() {
 | 
			
		||||
  return <div>Hello "/_authenticated/room/"!</div>
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										32
									
								
								src/routes/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/routes/index.tsx
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,32 @@
 | 
			
		|||
import {
 | 
			
		||||
  createFileRoute,
 | 
			
		||||
  Outlet,
 | 
			
		||||
  redirect,
 | 
			
		||||
} from "@tanstack/react-router";
 | 
			
		||||
import AppLayout from "@/layouts/app-layout";
 | 
			
		||||
export const Route = createFileRoute("/")({
 | 
			
		||||
  //  beforeLoad: async ({context}) => {
 | 
			
		||||
  //       const {authToken} = context.auth
 | 
			
		||||
  //       if (!authToken) {
 | 
			
		||||
  //         throw redirect({to: '/login'})
 | 
			
		||||
  //       }
 | 
			
		||||
  //     },
 | 
			
		||||
  head: () => ({
 | 
			
		||||
    meta: [
 | 
			
		||||
      {
 | 
			
		||||
        title: "Dashboard",
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  }),
 | 
			
		||||
  component: App,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function App() {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <AppLayout>
 | 
			
		||||
        <Outlet />
 | 
			
		||||
      </AppLayout>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										120
									
								
								src/styles.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								src/styles.css
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,120 @@
 | 
			
		|||
@import "tailwindcss";
 | 
			
		||||
@import "tw-animate-css";
 | 
			
		||||
 | 
			
		||||
@custom-variant dark (&:is(.dark *));
 | 
			
		||||
 | 
			
		||||
@theme inline {
 | 
			
		||||
  --radius-sm: calc(var(--radius) - 4px);
 | 
			
		||||
  --radius-md: calc(var(--radius) - 2px);
 | 
			
		||||
  --radius-lg: var(--radius);
 | 
			
		||||
  --radius-xl: calc(var(--radius) + 4px);
 | 
			
		||||
  --color-background: var(--background);
 | 
			
		||||
  --color-foreground: var(--foreground);
 | 
			
		||||
  --color-card: var(--card);
 | 
			
		||||
  --color-card-foreground: var(--card-foreground);
 | 
			
		||||
  --color-popover: var(--popover);
 | 
			
		||||
  --color-popover-foreground: var(--popover-foreground);
 | 
			
		||||
  --color-primary: var(--primary);
 | 
			
		||||
  --color-primary-foreground: var(--primary-foreground);
 | 
			
		||||
  --color-secondary: var(--secondary);
 | 
			
		||||
  --color-secondary-foreground: var(--secondary-foreground);
 | 
			
		||||
  --color-muted: var(--muted);
 | 
			
		||||
  --color-muted-foreground: var(--muted-foreground);
 | 
			
		||||
  --color-accent: var(--accent);
 | 
			
		||||
  --color-accent-foreground: var(--accent-foreground);
 | 
			
		||||
  --color-destructive: var(--destructive);
 | 
			
		||||
  --color-border: var(--border);
 | 
			
		||||
  --color-input: var(--input);
 | 
			
		||||
  --color-ring: var(--ring);
 | 
			
		||||
  --color-chart-1: var(--chart-1);
 | 
			
		||||
  --color-chart-2: var(--chart-2);
 | 
			
		||||
  --color-chart-3: var(--chart-3);
 | 
			
		||||
  --color-chart-4: var(--chart-4);
 | 
			
		||||
  --color-chart-5: var(--chart-5);
 | 
			
		||||
  --color-sidebar: var(--sidebar);
 | 
			
		||||
  --color-sidebar-foreground: var(--sidebar-foreground);
 | 
			
		||||
  --color-sidebar-primary: var(--sidebar-primary);
 | 
			
		||||
  --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
 | 
			
		||||
  --color-sidebar-accent: var(--sidebar-accent);
 | 
			
		||||
  --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
 | 
			
		||||
  --color-sidebar-border: var(--sidebar-border);
 | 
			
		||||
  --color-sidebar-ring: var(--sidebar-ring);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:root {
 | 
			
		||||
  --radius: 0.625rem;
 | 
			
		||||
  --background: oklch(1 0 0);
 | 
			
		||||
  --foreground: oklch(0.13 0.028 261.692);
 | 
			
		||||
  --card: oklch(1 0 0);
 | 
			
		||||
  --card-foreground: oklch(0.13 0.028 261.692);
 | 
			
		||||
  --popover: oklch(1 0 0);
 | 
			
		||||
  --popover-foreground: oklch(0.13 0.028 261.692);
 | 
			
		||||
  --primary: oklch(0.21 0.034 264.665);
 | 
			
		||||
  --primary-foreground: oklch(0.985 0.002 247.839);
 | 
			
		||||
  --secondary: oklch(0.967 0.003 264.542);
 | 
			
		||||
  --secondary-foreground: oklch(0.21 0.034 264.665);
 | 
			
		||||
  --muted: oklch(0.967 0.003 264.542);
 | 
			
		||||
  --muted-foreground: oklch(0.551 0.027 264.364);
 | 
			
		||||
  --accent: oklch(0.967 0.003 264.542);
 | 
			
		||||
  --accent-foreground: oklch(0.21 0.034 264.665);
 | 
			
		||||
  --destructive: oklch(0.577 0.245 27.325);
 | 
			
		||||
  --border: oklch(0.928 0.006 264.531);
 | 
			
		||||
  --input: oklch(0.928 0.006 264.531);
 | 
			
		||||
  --ring: oklch(0.707 0.022 261.325);
 | 
			
		||||
  --chart-1: oklch(0.646 0.222 41.116);
 | 
			
		||||
  --chart-2: oklch(0.6 0.118 184.704);
 | 
			
		||||
  --chart-3: oklch(0.398 0.07 227.392);
 | 
			
		||||
  --chart-4: oklch(0.828 0.189 84.429);
 | 
			
		||||
  --chart-5: oklch(0.769 0.188 70.08);
 | 
			
		||||
  --sidebar: oklch(0.985 0.002 247.839);
 | 
			
		||||
  --sidebar-foreground: oklch(0.13 0.028 261.692);
 | 
			
		||||
  --sidebar-primary: oklch(0.21 0.034 264.665);
 | 
			
		||||
  --sidebar-primary-foreground: oklch(0.985 0.002 247.839);
 | 
			
		||||
  --sidebar-accent: oklch(0.967 0.003 264.542);
 | 
			
		||||
  --sidebar-accent-foreground: oklch(0.21 0.034 264.665);
 | 
			
		||||
  --sidebar-border: oklch(0.928 0.006 264.531);
 | 
			
		||||
  --sidebar-ring: oklch(0.707 0.022 261.325);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dark {
 | 
			
		||||
  --background: oklch(0.13 0.028 261.692);
 | 
			
		||||
  --foreground: oklch(0.985 0.002 247.839);
 | 
			
		||||
  --card: oklch(0.21 0.034 264.665);
 | 
			
		||||
  --card-foreground: oklch(0.985 0.002 247.839);
 | 
			
		||||
  --popover: oklch(0.21 0.034 264.665);
 | 
			
		||||
  --popover-foreground: oklch(0.985 0.002 247.839);
 | 
			
		||||
  --primary: oklch(0.928 0.006 264.531);
 | 
			
		||||
  --primary-foreground: oklch(0.21 0.034 264.665);
 | 
			
		||||
  --secondary: oklch(0.278 0.033 256.848);
 | 
			
		||||
  --secondary-foreground: oklch(0.985 0.002 247.839);
 | 
			
		||||
  --muted: oklch(0.278 0.033 256.848);
 | 
			
		||||
  --muted-foreground: oklch(0.707 0.022 261.325);
 | 
			
		||||
  --accent: oklch(0.278 0.033 256.848);
 | 
			
		||||
  --accent-foreground: oklch(0.985 0.002 247.839);
 | 
			
		||||
  --destructive: oklch(0.704 0.191 22.216);
 | 
			
		||||
  --border: oklch(1 0 0 / 10%);
 | 
			
		||||
  --input: oklch(1 0 0 / 15%);
 | 
			
		||||
  --ring: oklch(0.551 0.027 264.364);
 | 
			
		||||
  --chart-1: oklch(0.488 0.243 264.376);
 | 
			
		||||
  --chart-2: oklch(0.696 0.17 162.48);
 | 
			
		||||
  --chart-3: oklch(0.769 0.188 70.08);
 | 
			
		||||
  --chart-4: oklch(0.627 0.265 303.9);
 | 
			
		||||
  --chart-5: oklch(0.645 0.246 16.439);
 | 
			
		||||
  --sidebar: oklch(0.21 0.034 264.665);
 | 
			
		||||
  --sidebar-foreground: oklch(0.985 0.002 247.839);
 | 
			
		||||
  --sidebar-primary: oklch(0.488 0.243 264.376);
 | 
			
		||||
  --sidebar-primary-foreground: oklch(0.985 0.002 247.839);
 | 
			
		||||
  --sidebar-accent: oklch(0.278 0.033 256.848);
 | 
			
		||||
  --sidebar-accent-foreground: oklch(0.985 0.002 247.839);
 | 
			
		||||
  --sidebar-border: oklch(1 0 0 / 10%);
 | 
			
		||||
  --sidebar-ring: oklch(0.551 0.027 264.364);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@layer base {
 | 
			
		||||
  * {
 | 
			
		||||
    @apply border-border outline-ring/50;
 | 
			
		||||
  }
 | 
			
		||||
  body {
 | 
			
		||||
    @apply bg-background text-foreground;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										4
									
								
								src/types/css.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/types/css.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,4 @@
 | 
			
		|||
declare module '*.css' {
 | 
			
		||||
  const content: string;
 | 
			
		||||
  export default content;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1
									
								
								src/types/vite-env.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/types/vite-env.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
/// <reference types="vite/client" />
 | 
			
		||||
							
								
								
									
										11
									
								
								tsconfig.app.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								tsconfig.app.json
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,11 @@
 | 
			
		|||
{
 | 
			
		||||
  "extends": "./tsconfig.json",
 | 
			
		||||
  "compilerOptions": {
 | 
			
		||||
    "outDir": "./dist",
 | 
			
		||||
    "composite": true,
 | 
			
		||||
    "noEmit": false,
 | 
			
		||||
    "types": []
 | 
			
		||||
  },
 | 
			
		||||
  "include": ["src/**/*.ts", "src/**/*.tsx"],
 | 
			
		||||
  "exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										38
									
								
								tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								tsconfig.json
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,38 @@
 | 
			
		|||
{
 | 
			
		||||
  "include": ["src"],
 | 
			
		||||
  "files": [],
 | 
			
		||||
  "references": [
 | 
			
		||||
    {
 | 
			
		||||
      "path": "./tsconfig.app.json"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "path": "./tsconfig.node.json"
 | 
			
		||||
    }
 | 
			
		||||
  ],
 | 
			
		||||
  "compilerOptions": {
 | 
			
		||||
    "target": "ES2022",
 | 
			
		||||
    "jsx": "react-jsx",
 | 
			
		||||
    "module": "ESNext",
 | 
			
		||||
    "lib": ["ES2022", "DOM", "DOM.Iterable"],
 | 
			
		||||
    "types": ["vite/client"],
 | 
			
		||||
 | 
			
		||||
    /* Bundler mode */
 | 
			
		||||
    "moduleResolution": "bundler",
 | 
			
		||||
    "allowImportingTsExtensions": false,
 | 
			
		||||
    "verbatimModuleSyntax": true,
 | 
			
		||||
    "noEmit": true,
 | 
			
		||||
 | 
			
		||||
    /* Linting */
 | 
			
		||||
    "skipLibCheck": true,
 | 
			
		||||
    "strict": true,
 | 
			
		||||
    "noUnusedLocals": true,
 | 
			
		||||
    "noUnusedParameters": true,
 | 
			
		||||
    "noFallthroughCasesInSwitch": true,
 | 
			
		||||
    "noUncheckedSideEffectImports": true,
 | 
			
		||||
     "baseUrl": ".",
 | 
			
		||||
    "paths": {
 | 
			
		||||
      "@/*": ["./src/*", "vite-env.d.ts"]
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										17
									
								
								tsconfig.node.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								tsconfig.node.json
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,17 @@
 | 
			
		|||
{
 | 
			
		||||
  "extends": "./tsconfig.json",
 | 
			
		||||
  "compilerOptions": {
 | 
			
		||||
    "composite": true,
 | 
			
		||||
    "outDir": "dist",
 | 
			
		||||
    "module": "ESNext",
 | 
			
		||||
    "types": ["node"],
 | 
			
		||||
    "baseUrl": ".",
 | 
			
		||||
    "paths": {
 | 
			
		||||
      "@/*": [
 | 
			
		||||
        "./src/*"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "noEmit": false,
 | 
			
		||||
  },
 | 
			
		||||
  "include": ["vite.config.ts"]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										26
									
								
								vite.config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								vite.config.ts
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,26 @@
 | 
			
		|||
// vite.config.ts
 | 
			
		||||
import { defineConfig } from 'vite'
 | 
			
		||||
import react from '@vitejs/plugin-react'
 | 
			
		||||
import { tanstackRouter } from '@tanstack/router-plugin/vite'
 | 
			
		||||
import tailwindcss from "@tailwindcss/vite"
 | 
			
		||||
import path from 'path'
 | 
			
		||||
 | 
			
		||||
// https://vitejs.dev/config/
 | 
			
		||||
export default defineConfig({
 | 
			
		||||
  plugins: [
 | 
			
		||||
    
 | 
			
		||||
    tanstackRouter({
 | 
			
		||||
      target: 'react',
 | 
			
		||||
      autoCodeSplitting: true,
 | 
			
		||||
    }),
 | 
			
		||||
    
 | 
			
		||||
    react(),
 | 
			
		||||
    tailwindcss()
 | 
			
		||||
    // ...,
 | 
			
		||||
  ],
 | 
			
		||||
  resolve: {
 | 
			
		||||
    alias: {
 | 
			
		||||
      "@": path.resolve(__dirname, "./src"),
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
})
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user