first commit
This commit is contained in:
		
							
								
								
									
										24
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
				
			|||||||
 | 
					# Logs
 | 
				
			||||||
 | 
					logs
 | 
				
			||||||
 | 
					*.log
 | 
				
			||||||
 | 
					npm-debug.log*
 | 
				
			||||||
 | 
					yarn-debug.log*
 | 
				
			||||||
 | 
					yarn-error.log*
 | 
				
			||||||
 | 
					pnpm-debug.log*
 | 
				
			||||||
 | 
					lerna-debug.log*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					node_modules
 | 
				
			||||||
 | 
					dist
 | 
				
			||||||
 | 
					dist-ssr
 | 
				
			||||||
 | 
					*.local
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Editor directories and files
 | 
				
			||||||
 | 
					.vscode/*
 | 
				
			||||||
 | 
					!.vscode/extensions.json
 | 
				
			||||||
 | 
					.idea
 | 
				
			||||||
 | 
					.DS_Store
 | 
				
			||||||
 | 
					*.suo
 | 
				
			||||||
 | 
					*.ntvs*
 | 
				
			||||||
 | 
					*.njsproj
 | 
				
			||||||
 | 
					*.sln
 | 
				
			||||||
 | 
					*.sw?
 | 
				
			||||||
							
								
								
									
										16
									
								
								.prettierrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								.prettierrc
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "semi": true,
 | 
				
			||||||
 | 
					  "tabWidth": 2,
 | 
				
			||||||
 | 
					  "printWidth": 100,
 | 
				
			||||||
 | 
					  "trailingComma": "all",
 | 
				
			||||||
 | 
					  "singleQuote": false,
 | 
				
			||||||
 | 
					  "jsxSingleQuote": false,
 | 
				
			||||||
 | 
					  "bracketSpacing": true,
 | 
				
			||||||
 | 
					  "bracketSameLine": false,
 | 
				
			||||||
 | 
					  "arrowParens": "always",
 | 
				
			||||||
 | 
					  "endOfLine": "lf",
 | 
				
			||||||
 | 
					  "quoteProps": "as-needed",
 | 
				
			||||||
 | 
					  "plugins": ["prettier-plugin-tailwindcss"],
 | 
				
			||||||
 | 
					  "tailwindConfig": "./tailwind.config.js",
 | 
				
			||||||
 | 
					  "embeddedLanguageFormatting": "auto"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										52
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,52 @@
 | 
				
			|||||||
 | 
					# IoPlot Dashboard UI - Fully writen by AI
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# React + TypeScript + Vite
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Currently, two official plugins are available:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
 | 
				
			||||||
 | 
					- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Expanding the ESLint configuration
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Configure the top-level `parserOptions` property like this:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```js
 | 
				
			||||||
 | 
					export default tseslint.config({
 | 
				
			||||||
 | 
					  languageOptions: {
 | 
				
			||||||
 | 
					    // other options...
 | 
				
			||||||
 | 
					    parserOptions: {
 | 
				
			||||||
 | 
					      project: ['./tsconfig.node.json', './tsconfig.app.json'],
 | 
				
			||||||
 | 
					      tsconfigRootDir: import.meta.dirname,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
 | 
				
			||||||
 | 
					- Optionally add `...tseslint.configs.stylisticTypeChecked`
 | 
				
			||||||
 | 
					- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```js
 | 
				
			||||||
 | 
					// eslint.config.js
 | 
				
			||||||
 | 
					import react from 'eslint-plugin-react'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default tseslint.config({
 | 
				
			||||||
 | 
					  // Set the react version
 | 
				
			||||||
 | 
					  settings: { react: { version: '18.3' } },
 | 
				
			||||||
 | 
					  plugins: {
 | 
				
			||||||
 | 
					    // Add the react plugin
 | 
				
			||||||
 | 
					    react,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  rules: {
 | 
				
			||||||
 | 
					    // other rules...
 | 
				
			||||||
 | 
					    // Enable its recommended rules
 | 
				
			||||||
 | 
					    ...react.configs.recommended.rules,
 | 
				
			||||||
 | 
					    ...react.configs['jsx-runtime'].rules,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
							
								
								
									
										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": "tailwind.config.js",
 | 
				
			||||||
 | 
					    "css": "src/index.css",
 | 
				
			||||||
 | 
					    "baseColor": "zinc",
 | 
				
			||||||
 | 
					    "cssVariables": true,
 | 
				
			||||||
 | 
					    "prefix": ""
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "aliases": {
 | 
				
			||||||
 | 
					    "components": "@/components",
 | 
				
			||||||
 | 
					    "utils": "@/lib/utils",
 | 
				
			||||||
 | 
					    "ui": "@/components/ui",
 | 
				
			||||||
 | 
					    "lib": "@/lib",
 | 
				
			||||||
 | 
					    "hooks": "@/hooks"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "iconLibrary": "lucide"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										123
									
								
								eslint.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								eslint.config.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,123 @@
 | 
				
			|||||||
 | 
					// @ts-check
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import js from "@eslint/js";
 | 
				
			||||||
 | 
					import * as importPlugin from "eslint-plugin-import";
 | 
				
			||||||
 | 
					import reactPlugin from "eslint-plugin-react";
 | 
				
			||||||
 | 
					import reactHooks from "eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js";
 | 
				
			||||||
 | 
					import * as reactRefresh from "eslint-plugin-react-refresh";
 | 
				
			||||||
 | 
					import globals from "globals";
 | 
				
			||||||
 | 
					import tseslint from "typescript-eslint";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default tseslint.config(
 | 
				
			||||||
 | 
					  { ignores: ["dist"] },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    extends: [
 | 
				
			||||||
 | 
					      js.configs.recommended,
 | 
				
			||||||
 | 
					      ...tseslint.configs.recommended,
 | 
				
			||||||
 | 
					      ...tseslint.configs.stylistic,
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    files: ["**/*.{ts,tsx}"],
 | 
				
			||||||
 | 
					    languageOptions: {
 | 
				
			||||||
 | 
					      ecmaVersion: 2020,
 | 
				
			||||||
 | 
					      globals: {
 | 
				
			||||||
 | 
					        ...globals.browser,
 | 
				
			||||||
 | 
					        React: "readonly",
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      parser: tseslint.parser,
 | 
				
			||||||
 | 
					      parserOptions: {
 | 
				
			||||||
 | 
					        project: "./tsconfig.json",
 | 
				
			||||||
 | 
					        tsconfigRootDir: import.meta.dirname,
 | 
				
			||||||
 | 
					        ecmaFeatures: {
 | 
				
			||||||
 | 
					          jsx: true,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    plugins: {
 | 
				
			||||||
 | 
					      react: reactPlugin,
 | 
				
			||||||
 | 
					      "@typescript-eslint": tseslint.plugin,
 | 
				
			||||||
 | 
					      import: importPlugin,
 | 
				
			||||||
 | 
					      "react-hooks": reactHooks,
 | 
				
			||||||
 | 
					      "react-refresh": reactRefresh,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    settings: {
 | 
				
			||||||
 | 
					      react: {
 | 
				
			||||||
 | 
					        version: "detect",
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "import/parsers": {
 | 
				
			||||||
 | 
					        "@typescript-eslint/parser": [".ts", ".tsx"],
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "import/resolver": {
 | 
				
			||||||
 | 
					        typescript: {
 | 
				
			||||||
 | 
					          project: "./tsconfig.json",
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        node: true,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    rules: {
 | 
				
			||||||
 | 
					      ...reactHooks.configs.recommended.rules,
 | 
				
			||||||
 | 
					      ...reactPlugin.configs.recommended.rules,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Import rules
 | 
				
			||||||
 | 
					      "import/no-unresolved": "error",
 | 
				
			||||||
 | 
					      "import/named": "off",
 | 
				
			||||||
 | 
					      "import/default": "warn",
 | 
				
			||||||
 | 
					      "import/export": "error",
 | 
				
			||||||
 | 
					      "import/no-duplicates": "warn",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // React Refresh - modified to be less strict for component files
 | 
				
			||||||
 | 
					      "react-refresh/only-export-components": [
 | 
				
			||||||
 | 
					        "off",
 | 
				
			||||||
 | 
					        { allowConstantExport: true },
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // TypeScript
 | 
				
			||||||
 | 
					      "@typescript-eslint/no-unused-vars": "off",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Tailwind-friendly rules
 | 
				
			||||||
 | 
					      "react/no-unknown-property": [
 | 
				
			||||||
 | 
					        "error",
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          ignore: [
 | 
				
			||||||
 | 
					            "class",
 | 
				
			||||||
 | 
					            "css",
 | 
				
			||||||
 | 
					            "tw",
 | 
				
			||||||
 | 
					            "args",
 | 
				
			||||||
 | 
					            "position",
 | 
				
			||||||
 | 
					            "intensity",
 | 
				
			||||||
 | 
					            "rotation",
 | 
				
			||||||
 | 
					            "vertexShader",
 | 
				
			||||||
 | 
					            "fragmentShader",
 | 
				
			||||||
 | 
					            "uniforms",
 | 
				
			||||||
 | 
					            "side",
 | 
				
			||||||
 | 
					            "cmdk-input-wrapper",
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Turn off rules that are too strict
 | 
				
			||||||
 | 
					      "@typescript-eslint/naming-convention": "off",
 | 
				
			||||||
 | 
					      "@typescript-eslint/no-explicit-any": "warn",
 | 
				
			||||||
 | 
					      "@typescript-eslint/no-unsafe-assignment": "off",
 | 
				
			||||||
 | 
					      "@typescript-eslint/no-unsafe-call": "off",
 | 
				
			||||||
 | 
					      "@typescript-eslint/no-unsafe-member-access": "off",
 | 
				
			||||||
 | 
					      "@typescript-eslint/no-unsafe-return": "off",
 | 
				
			||||||
 | 
					      "@typescript-eslint/no-unsafe-argument": "off",
 | 
				
			||||||
 | 
					      "@typescript-eslint/restrict-template-expressions": "off",
 | 
				
			||||||
 | 
					      "@typescript-eslint/no-redundant-type-constituents": "off",
 | 
				
			||||||
 | 
					      "@typescript-eslint/no-floating-promises": "off",
 | 
				
			||||||
 | 
					      "@typescript-eslint/unbound-method": "off",
 | 
				
			||||||
 | 
					      "@typescript-eslint/require-await": "warn",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // React specific
 | 
				
			||||||
 | 
					      "react/react-in-jsx-scope": "off",
 | 
				
			||||||
 | 
					      "react/prop-types": "off",
 | 
				
			||||||
 | 
					      "react/jsx-no-target-blank": "warn",
 | 
				
			||||||
 | 
					      "react/jsx-key": ["warn", { checkFragmentShorthand: true }],
 | 
				
			||||||
 | 
					      "react/no-unescaped-entities": "off",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // React Hooks
 | 
				
			||||||
 | 
					      "react-hooks/rules-of-hooks": "error",
 | 
				
			||||||
 | 
					      "react-hooks/exhaustive-deps": "warn",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
							
								
								
									
										17
									
								
								index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								index.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html lang="en">
 | 
				
			||||||
 | 
					  <head>
 | 
				
			||||||
 | 
					    <meta charset="UTF-8" />
 | 
				
			||||||
 | 
					    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
 | 
				
			||||||
 | 
					    <link rel="canonical" href="https://io.mahdium.ir/" />
 | 
				
			||||||
 | 
					    <link rel="preload" href="/assets/index-DvpC3T31.css" as="style" />
 | 
				
			||||||
 | 
					    <link rel="preload" href="/assets/index-B0uGbMrj.js" as="script" />
 | 
				
			||||||
 | 
					    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 | 
				
			||||||
 | 
					    <meta name="description" content="IoT Plotter - Monitor, visualize, and analyze your IoT data in real-time" />
 | 
				
			||||||
 | 
					    <title>IoT Plotter</title>
 | 
				
			||||||
 | 
					  </head>
 | 
				
			||||||
 | 
					  <body>
 | 
				
			||||||
 | 
					    <div id="root"></div>
 | 
				
			||||||
 | 
					    <script type="module" src="/src/main.tsx"></script>
 | 
				
			||||||
 | 
					  </body>
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
							
								
								
									
										24
									
								
								netlify.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								netlify.toml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
				
			|||||||
 | 
					[images]
 | 
				
			||||||
 | 
					  remote_images = ["https://source.unsplash.com/.*", "https://images.unsplash.com/.*", "https://ext.same-assets.com/.*", "https://ugc.same-assets.com/.*"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[build]
 | 
				
			||||||
 | 
					  command = "bun install && bun run build"
 | 
				
			||||||
 | 
					  publish = "dist"
 | 
				
			||||||
 | 
					  functions = "netlify/functions"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[build.environment]
 | 
				
			||||||
 | 
					  NODE_VERSION = "20"
 | 
				
			||||||
 | 
					  VITE_API_URL = "https://io-a.monasefloadbalancer.ir"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[redirects]]
 | 
				
			||||||
 | 
					  from = "/*"
 | 
				
			||||||
 | 
					  to = "/index.html"
 | 
				
			||||||
 | 
					  status = 200
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[dev]
 | 
				
			||||||
 | 
					  command = "bun run dev"
 | 
				
			||||||
 | 
					  port = 5173
 | 
				
			||||||
 | 
					  targetPort = 5173
 | 
				
			||||||
 | 
					  publish = "dist"
 | 
				
			||||||
 | 
					  autoLaunch = true
 | 
				
			||||||
 | 
					  framework = "vite"
 | 
				
			||||||
							
								
								
									
										92
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,92 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "name": "react-vite-tailwind",
 | 
				
			||||||
 | 
					  "private": true,
 | 
				
			||||||
 | 
					  "version": "0.0.0",
 | 
				
			||||||
 | 
					  "type": "module",
 | 
				
			||||||
 | 
					  "scripts": {
 | 
				
			||||||
 | 
					    "dev": "vite --host 0.0.0.0",
 | 
				
			||||||
 | 
					    "build": "tsc -b && vite build --outDir dist",
 | 
				
			||||||
 | 
					    "lint": "eslint src --fix && tsc --noEmit",
 | 
				
			||||||
 | 
					    "format": "prettier --write \"src/**/*.{ts,tsx}\"",
 | 
				
			||||||
 | 
					    "preview": "vite preview"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "dependencies": {
 | 
				
			||||||
 | 
					    "@hookform/resolvers": "^4.1.3",
 | 
				
			||||||
 | 
					    "@radix-ui/react-accordion": "^1.2.2",
 | 
				
			||||||
 | 
					    "@radix-ui/react-alert-dialog": "^1.1.4",
 | 
				
			||||||
 | 
					    "@radix-ui/react-aspect-ratio": "^1.1.1",
 | 
				
			||||||
 | 
					    "@radix-ui/react-avatar": "^1.1.3",
 | 
				
			||||||
 | 
					    "@radix-ui/react-checkbox": "^1.1.3",
 | 
				
			||||||
 | 
					    "@radix-ui/react-collapsible": "^1.1.2",
 | 
				
			||||||
 | 
					    "@radix-ui/react-context-menu": "^2.2.4",
 | 
				
			||||||
 | 
					    "@radix-ui/react-dialog": "^1.1.6",
 | 
				
			||||||
 | 
					    "@radix-ui/react-dropdown-menu": "^2.1.6",
 | 
				
			||||||
 | 
					    "@radix-ui/react-hover-card": "^1.1.4",
 | 
				
			||||||
 | 
					    "@radix-ui/react-label": "^2.1.2",
 | 
				
			||||||
 | 
					    "@radix-ui/react-menubar": "^1.1.4",
 | 
				
			||||||
 | 
					    "@radix-ui/react-navigation-menu": "^1.2.3",
 | 
				
			||||||
 | 
					    "@radix-ui/react-popover": "^1.1.6",
 | 
				
			||||||
 | 
					    "@radix-ui/react-progress": "^1.1.1",
 | 
				
			||||||
 | 
					    "@radix-ui/react-radio-group": "^1.2.2",
 | 
				
			||||||
 | 
					    "@radix-ui/react-scroll-area": "^1.2.2",
 | 
				
			||||||
 | 
					    "@radix-ui/react-select": "^2.1.6",
 | 
				
			||||||
 | 
					    "@radix-ui/react-separator": "^1.1.2",
 | 
				
			||||||
 | 
					    "@radix-ui/react-slider": "^1.2.2",
 | 
				
			||||||
 | 
					    "@radix-ui/react-slot": "^1.1.2",
 | 
				
			||||||
 | 
					    "@radix-ui/react-switch": "^1.1.2",
 | 
				
			||||||
 | 
					    "@radix-ui/react-tabs": "^1.1.3",
 | 
				
			||||||
 | 
					    "@radix-ui/react-toast": "^1.2.6",
 | 
				
			||||||
 | 
					    "@radix-ui/react-toggle": "^1.1.1",
 | 
				
			||||||
 | 
					    "@radix-ui/react-toggle-group": "^1.1.1",
 | 
				
			||||||
 | 
					    "@radix-ui/react-tooltip": "^1.1.6",
 | 
				
			||||||
 | 
					    "@tanstack/react-query": "^5.69.0",
 | 
				
			||||||
 | 
					    "axios": "^1.8.3",
 | 
				
			||||||
 | 
					    "cmdk": "^1.0.0",
 | 
				
			||||||
 | 
					    "date-fns": "^4.1.0",
 | 
				
			||||||
 | 
					    "embla-carousel-react": "^8.5.2",
 | 
				
			||||||
 | 
					    "input-otp": "^1.4.2",
 | 
				
			||||||
 | 
					    "jwt-decode": "^4.0.0",
 | 
				
			||||||
 | 
					    "next-themes": "^0.4.4",
 | 
				
			||||||
 | 
					    "react": "^18.3.1",
 | 
				
			||||||
 | 
					    "react-day-picker": "8.10.1",
 | 
				
			||||||
 | 
					    "react-dom": "^18.3.1",
 | 
				
			||||||
 | 
					    "react-hook-form": "^7.54.2",
 | 
				
			||||||
 | 
					    "react-resizable-panels": "^2.1.7",
 | 
				
			||||||
 | 
					    "react-router-dom": "^7.4.0",
 | 
				
			||||||
 | 
					    "recharts": "^2.15.1",
 | 
				
			||||||
 | 
					    "sonner": "^2.0.1",
 | 
				
			||||||
 | 
					    "vaul": "^1.1.2",
 | 
				
			||||||
 | 
					    "zod": "^3.24.2",
 | 
				
			||||||
 | 
					    "zustand": "^5.0.3"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "devDependencies": {
 | 
				
			||||||
 | 
					    "@eslint/js": "^9.18.0",
 | 
				
			||||||
 | 
					    "@types/node": "^22.10.7",
 | 
				
			||||||
 | 
					    "@types/react": "^18.3.18",
 | 
				
			||||||
 | 
					    "@types/react-dom": "^18.3.5",
 | 
				
			||||||
 | 
					    "@typescript-eslint/eslint-plugin": "^8.20.0",
 | 
				
			||||||
 | 
					    "@typescript-eslint/parser": "^8.20.0",
 | 
				
			||||||
 | 
					    "@vitejs/plugin-react": "^4.3.4",
 | 
				
			||||||
 | 
					    "autoprefixer": "^10.4.20",
 | 
				
			||||||
 | 
					    "class-variance-authority": "^0.7.1",
 | 
				
			||||||
 | 
					    "clsx": "^2.1.1",
 | 
				
			||||||
 | 
					    "eslint": "^9.18.0",
 | 
				
			||||||
 | 
					    "eslint-config-prettier": "^10.0.1",
 | 
				
			||||||
 | 
					    "eslint-import-resolver-typescript": "^3.7.0",
 | 
				
			||||||
 | 
					    "eslint-plugin-import": "^2.31.0",
 | 
				
			||||||
 | 
					    "eslint-plugin-react": "^7.37.4",
 | 
				
			||||||
 | 
					    "eslint-plugin-react-hooks": "^5.1.0",
 | 
				
			||||||
 | 
					    "eslint-plugin-react-refresh": "^0.4.18",
 | 
				
			||||||
 | 
					    "globals": "^15.14.0",
 | 
				
			||||||
 | 
					    "lucide-react": "^0.471.2",
 | 
				
			||||||
 | 
					    "postcss": "^8.5.1",
 | 
				
			||||||
 | 
					    "prettier": "^3.4.2",
 | 
				
			||||||
 | 
					    "prettier-plugin-tailwindcss": "^0.6.10",
 | 
				
			||||||
 | 
					    "tailwind-merge": "^2.6.0",
 | 
				
			||||||
 | 
					    "tailwindcss": "^3.4.17",
 | 
				
			||||||
 | 
					    "tailwindcss-animate": "^1.0.7",
 | 
				
			||||||
 | 
					    "typescript": "~5.6.2",
 | 
				
			||||||
 | 
					    "typescript-eslint": "^8.20.0",
 | 
				
			||||||
 | 
					    "vite": "^6.0.5"
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										6
									
								
								postcss.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								postcss.config.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					export default {
 | 
				
			||||||
 | 
					  plugins: {
 | 
				
			||||||
 | 
					    tailwindcss: {},
 | 
				
			||||||
 | 
					    autoprefixer: {},
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										1
									
								
								public/CNAME
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								public/CNAME
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					io.mahdium.ir
 | 
				
			||||||
							
								
								
									
										16
									
								
								public/favicon.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								public/favicon.svg
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-activity-square">
 | 
				
			||||||
 | 
					  <style>
 | 
				
			||||||
 | 
					    @media (prefers-color-scheme: dark) {
 | 
				
			||||||
 | 
					      :root {
 | 
				
			||||||
 | 
					        stroke: #ffffff;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    @media (prefers-color-scheme: light) {
 | 
				
			||||||
 | 
					      :root {
 | 
				
			||||||
 | 
					        stroke: #0369a1;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  </style>
 | 
				
			||||||
 | 
					  <rect width="18" height="18" x="3" y="3" rx="2" />
 | 
				
			||||||
 | 
					  <path d="M17 12h-2l-2 5-2-10-2 5H7" />
 | 
				
			||||||
 | 
					</svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 533 B  | 
							
								
								
									
										62
									
								
								src/App.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								src/App.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,62 @@
 | 
				
			|||||||
 | 
					import { BrowserRouter as Router, Routes, Route, Navigate } from "react-router-dom";
 | 
				
			||||||
 | 
					import { ThemeProvider } from "@/lib/theme-provider";
 | 
				
			||||||
 | 
					import { Layout } from "@/components/Layout";
 | 
				
			||||||
 | 
					import { ProtectedRoute } from "@/components/ProtectedRoute";
 | 
				
			||||||
 | 
					import { HomePage } from "@/pages/HomePage";
 | 
				
			||||||
 | 
					import { LoginPage } from "@/pages/LoginPage";
 | 
				
			||||||
 | 
					import { RegisterPage } from "@/pages/RegisterPage";
 | 
				
			||||||
 | 
					import { DashboardPage } from "@/pages/DashboardPage";
 | 
				
			||||||
 | 
					import { ApiKeysPage } from "@/pages/ApiKeysPage";
 | 
				
			||||||
 | 
					import { ProfilePage } from "@/pages/ProfilePage";
 | 
				
			||||||
 | 
					import { VerifyEmail } from "@/pages/VerifyEmail";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function App() {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <ThemeProvider defaultTheme="system">
 | 
				
			||||||
 | 
					      <Router>
 | 
				
			||||||
 | 
					        <Routes>
 | 
				
			||||||
 | 
					          <Route path="/" element={<Layout />}>
 | 
				
			||||||
 | 
					            <Route index element={<HomePage />} />
 | 
				
			||||||
 | 
					            <Route path="login" element={<LoginPage />} />
 | 
				
			||||||
 | 
					            <Route path="register" element={<RegisterPage />} />
 | 
				
			||||||
 | 
					            <Route
 | 
				
			||||||
 | 
					              path="dashboard"
 | 
				
			||||||
 | 
					              element={
 | 
				
			||||||
 | 
					                <ProtectedRoute>
 | 
				
			||||||
 | 
					                  <DashboardPage />
 | 
				
			||||||
 | 
					                </ProtectedRoute>
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					            <Route
 | 
				
			||||||
 | 
					              path="api-keys"
 | 
				
			||||||
 | 
					              element={
 | 
				
			||||||
 | 
					                <ProtectedRoute>
 | 
				
			||||||
 | 
					                  <ApiKeysPage />
 | 
				
			||||||
 | 
					                </ProtectedRoute>
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					            <Route
 | 
				
			||||||
 | 
					              path="profile"
 | 
				
			||||||
 | 
					              element={
 | 
				
			||||||
 | 
					                <ProtectedRoute>
 | 
				
			||||||
 | 
					                  <ProfilePage />
 | 
				
			||||||
 | 
					                </ProtectedRoute>
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					            <Route
 | 
				
			||||||
 | 
					              path="verifyemail"
 | 
				
			||||||
 | 
					              element={
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                  <VerifyEmail />
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					            <Route path="*" element={<Navigate to="/" replace />} />
 | 
				
			||||||
 | 
					          </Route>
 | 
				
			||||||
 | 
					        </Routes>
 | 
				
			||||||
 | 
					      </Router>
 | 
				
			||||||
 | 
					    </ThemeProvider>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default App;
 | 
				
			||||||
							
								
								
									
										130
									
								
								src/components/ApiKeyItem.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								src/components/ApiKeyItem.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,130 @@
 | 
				
			|||||||
 | 
					import { useState } from "react";
 | 
				
			||||||
 | 
					import { formatDateTime } from "@/lib/timeRanges";
 | 
				
			||||||
 | 
					import { ApiKey } from "@/lib/types";
 | 
				
			||||||
 | 
					import { Button } from "@/components/ui/button";
 | 
				
			||||||
 | 
					import { Card, CardContent } from "@/components/ui/card";
 | 
				
			||||||
 | 
					import { toast } from "sonner";
 | 
				
			||||||
 | 
					import { Copy, Eye, EyeOff, Trash2 } from "lucide-react";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  Dialog,
 | 
				
			||||||
 | 
					  DialogContent,
 | 
				
			||||||
 | 
					  DialogDescription,
 | 
				
			||||||
 | 
					  DialogFooter,
 | 
				
			||||||
 | 
					  DialogHeader,
 | 
				
			||||||
 | 
					  DialogTitle,
 | 
				
			||||||
 | 
					} from "@/components/ui/dialog";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface ApiKeyItemProps {
 | 
				
			||||||
 | 
					  apiKey: ApiKey;
 | 
				
			||||||
 | 
					  onDelete: (key: string) => Promise<void>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function ApiKeyItem({ apiKey, onDelete }: ApiKeyItemProps) {
 | 
				
			||||||
 | 
					  const [isVisible, setIsVisible] = useState(false);
 | 
				
			||||||
 | 
					  const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
 | 
				
			||||||
 | 
					  const [isDeleting, setIsDeleting] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const toggleVisibility = () => {
 | 
				
			||||||
 | 
					    setIsVisible(!isVisible);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleCopyKey = () => {
 | 
				
			||||||
 | 
					    navigator.clipboard.writeText(apiKey.key);
 | 
				
			||||||
 | 
					    toast.success("API key copied to clipboard");
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleDelete = async () => {
 | 
				
			||||||
 | 
					    setIsDeleting(true);
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      await onDelete(apiKey.key);
 | 
				
			||||||
 | 
					      toast.success("API key deleted successfully");
 | 
				
			||||||
 | 
					      setIsDeleteDialogOpen(false);
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      toast.error("Failed to delete API key");
 | 
				
			||||||
 | 
					      console.error(error);
 | 
				
			||||||
 | 
					    } finally {
 | 
				
			||||||
 | 
					      setIsDeleting(false);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const displayKey = isVisible
 | 
				
			||||||
 | 
					    ? apiKey.key
 | 
				
			||||||
 | 
					    : apiKey.key.substring(0, 4) + "•".repeat(apiKey.key.length - 8) + apiKey.key.substring(apiKey.key.length - 4);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Card className="w-full shadow-sm">
 | 
				
			||||||
 | 
					      <CardContent className="p-4">
 | 
				
			||||||
 | 
					        <div className="flex flex-col gap-3">
 | 
				
			||||||
 | 
					          <div className="flex items-center justify-between">
 | 
				
			||||||
 | 
					            <div className="text-sm text-muted-foreground">
 | 
				
			||||||
 | 
					              Created: {formatDateTime(apiKey.createdAt)}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div className="flex items-center gap-1">
 | 
				
			||||||
 | 
					              <Button
 | 
				
			||||||
 | 
					                variant="ghost"
 | 
				
			||||||
 | 
					                size="icon"
 | 
				
			||||||
 | 
					                className="h-7 w-7"
 | 
				
			||||||
 | 
					                onClick={toggleVisibility}
 | 
				
			||||||
 | 
					                title={isVisible ? "Hide API Key" : "Show API Key"}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                {isVisible ? (
 | 
				
			||||||
 | 
					                  <EyeOff className="h-4 w-4" />
 | 
				
			||||||
 | 
					                ) : (
 | 
				
			||||||
 | 
					                  <Eye className="h-4 w-4" />
 | 
				
			||||||
 | 
					                )}
 | 
				
			||||||
 | 
					              </Button>
 | 
				
			||||||
 | 
					              <Button
 | 
				
			||||||
 | 
					                variant="ghost"
 | 
				
			||||||
 | 
					                size="icon"
 | 
				
			||||||
 | 
					                className="h-7 w-7"
 | 
				
			||||||
 | 
					                onClick={handleCopyKey}
 | 
				
			||||||
 | 
					                title="Copy API Key"
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <Copy className="h-4 w-4" />
 | 
				
			||||||
 | 
					              </Button>
 | 
				
			||||||
 | 
					              <Button
 | 
				
			||||||
 | 
					                variant="ghost"
 | 
				
			||||||
 | 
					                size="icon"
 | 
				
			||||||
 | 
					                className="h-7 w-7 text-destructive hover:text-destructive"
 | 
				
			||||||
 | 
					                onClick={() => setIsDeleteDialogOpen(true)}
 | 
				
			||||||
 | 
					                title="Delete API Key"
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <Trash2 className="h-4 w-4" />
 | 
				
			||||||
 | 
					              </Button>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <div className="font-mono text-sm p-2 bg-secondary/30 rounded-md break-all">
 | 
				
			||||||
 | 
					            {displayKey}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </CardContent>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <Dialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
 | 
				
			||||||
 | 
					        <DialogContent>
 | 
				
			||||||
 | 
					          <DialogHeader>
 | 
				
			||||||
 | 
					            <DialogTitle>Delete API Key</DialogTitle>
 | 
				
			||||||
 | 
					            <DialogDescription>
 | 
				
			||||||
 | 
					              Are you sure you want to delete this API key? This action cannot be undone. Any devices using this key will no longer be able to send data.
 | 
				
			||||||
 | 
					            </DialogDescription>
 | 
				
			||||||
 | 
					          </DialogHeader>
 | 
				
			||||||
 | 
					          <DialogFooter>
 | 
				
			||||||
 | 
					            <Button
 | 
				
			||||||
 | 
					              variant="outline"
 | 
				
			||||||
 | 
					              onClick={() => setIsDeleteDialogOpen(false)}
 | 
				
			||||||
 | 
					              disabled={isDeleting}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              Cancel
 | 
				
			||||||
 | 
					            </Button>
 | 
				
			||||||
 | 
					            <Button
 | 
				
			||||||
 | 
					              variant="destructive"
 | 
				
			||||||
 | 
					              onClick={handleDelete}
 | 
				
			||||||
 | 
					              disabled={isDeleting}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              {isDeleting ? "Deleting..." : "Delete"}
 | 
				
			||||||
 | 
					            </Button>
 | 
				
			||||||
 | 
					          </DialogFooter>
 | 
				
			||||||
 | 
					        </DialogContent>
 | 
				
			||||||
 | 
					      </Dialog>
 | 
				
			||||||
 | 
					    </Card>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										95
									
								
								src/components/FeedActions.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								src/components/FeedActions.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,95 @@
 | 
				
			|||||||
 | 
					import { useState } from "react";
 | 
				
			||||||
 | 
					import { Copy, Trash2 } from "lucide-react";
 | 
				
			||||||
 | 
					import { toast } from "sonner";
 | 
				
			||||||
 | 
					import { Button } from "@/components/ui/button";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  Dialog,
 | 
				
			||||||
 | 
					  DialogContent,
 | 
				
			||||||
 | 
					  DialogDescription,
 | 
				
			||||||
 | 
					  DialogFooter,
 | 
				
			||||||
 | 
					  DialogHeader,
 | 
				
			||||||
 | 
					  DialogTitle,
 | 
				
			||||||
 | 
					} from "@/components/ui/dialog";
 | 
				
			||||||
 | 
					import { Feed } from "@/lib/types";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface FeedActionsProps {
 | 
				
			||||||
 | 
					  feed: Feed;
 | 
				
			||||||
 | 
					  onDelete: (feedId: string) => Promise<void>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function FeedActions({ feed, onDelete }: FeedActionsProps) {
 | 
				
			||||||
 | 
					  const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
 | 
				
			||||||
 | 
					  const [isDeleting, setIsDeleting] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleCopyId = () => {
 | 
				
			||||||
 | 
					    navigator.clipboard.writeText(feed.id);
 | 
				
			||||||
 | 
					    toast.success("Feed ID copied to clipboard");
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleDelete = async () => {
 | 
				
			||||||
 | 
					    setIsDeleting(true);
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      await onDelete(feed.id);
 | 
				
			||||||
 | 
					      toast.success("Feed deleted successfully");
 | 
				
			||||||
 | 
					      setIsDeleteDialogOpen(false);
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      toast.error("Failed to delete feed");
 | 
				
			||||||
 | 
					      console.error(error);
 | 
				
			||||||
 | 
					    } finally {
 | 
				
			||||||
 | 
					      setIsDeleting(false);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      <div className="flex items-center gap-1 shrink-0">
 | 
				
			||||||
 | 
					        <Button
 | 
				
			||||||
 | 
					          variant="ghost"
 | 
				
			||||||
 | 
					          size="icon"
 | 
				
			||||||
 | 
					          className="h-6 w-6"
 | 
				
			||||||
 | 
					          onClick={handleCopyId}
 | 
				
			||||||
 | 
					          title="Copy Feed ID"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <Copy className="h-3 w-3" />
 | 
				
			||||||
 | 
					        </Button>
 | 
				
			||||||
 | 
					        <Button
 | 
				
			||||||
 | 
					          variant="ghost"
 | 
				
			||||||
 | 
					          size="icon"
 | 
				
			||||||
 | 
					          className="h-6 w-6 text-destructive hover:text-destructive"
 | 
				
			||||||
 | 
					          onClick={() => setIsDeleteDialogOpen(true)}
 | 
				
			||||||
 | 
					          title="Delete Feed"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <Trash2 className="h-3 w-3" />
 | 
				
			||||||
 | 
					        </Button>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <Dialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
 | 
				
			||||||
 | 
					        <DialogContent>
 | 
				
			||||||
 | 
					          <DialogHeader>
 | 
				
			||||||
 | 
					            <DialogTitle>Delete Feed</DialogTitle>
 | 
				
			||||||
 | 
					            <DialogDescription>
 | 
				
			||||||
 | 
					              Are you sure you want to delete this feed? This action cannot be undone and all data
 | 
				
			||||||
 | 
					              associated with this feed will be permanently deleted.
 | 
				
			||||||
 | 
					            </DialogDescription>
 | 
				
			||||||
 | 
					          </DialogHeader>
 | 
				
			||||||
 | 
					          <DialogFooter>
 | 
				
			||||||
 | 
					            <Button
 | 
				
			||||||
 | 
					              variant="outline"
 | 
				
			||||||
 | 
					              onClick={() => setIsDeleteDialogOpen(false)}
 | 
				
			||||||
 | 
					              disabled={isDeleting}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              Cancel
 | 
				
			||||||
 | 
					            </Button>
 | 
				
			||||||
 | 
					            <Button
 | 
				
			||||||
 | 
					              variant="destructive"
 | 
				
			||||||
 | 
					              onClick={handleDelete}
 | 
				
			||||||
 | 
					              disabled={isDeleting}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              {isDeleting ? "Deleting..." : "Delete"}
 | 
				
			||||||
 | 
					            </Button>
 | 
				
			||||||
 | 
					          </DialogFooter>
 | 
				
			||||||
 | 
					        </DialogContent>
 | 
				
			||||||
 | 
					      </Dialog>
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										130
									
								
								src/components/FeedChart.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								src/components/FeedChart.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,130 @@
 | 
				
			|||||||
 | 
					import { useMemo } from "react";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  LineChart,
 | 
				
			||||||
 | 
					  Line,
 | 
				
			||||||
 | 
					  XAxis,
 | 
				
			||||||
 | 
					  YAxis,
 | 
				
			||||||
 | 
					  CartesianGrid,
 | 
				
			||||||
 | 
					  Tooltip,
 | 
				
			||||||
 | 
					  ResponsiveContainer,
 | 
				
			||||||
 | 
					  TooltipProps,
 | 
				
			||||||
 | 
					} from "recharts";
 | 
				
			||||||
 | 
					import { format } from "date-fns";
 | 
				
			||||||
 | 
					import { FeedDataPoint } from "@/lib/types";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface FeedChartProps {
 | 
				
			||||||
 | 
					  data: FeedDataPoint[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface FormattedDataPoint {
 | 
				
			||||||
 | 
					  timestamp: number;
 | 
				
			||||||
 | 
					  value: number;
 | 
				
			||||||
 | 
					  formattedTime: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Function to downsample data points for cleaner visualization
 | 
				
			||||||
 | 
					const downsampleData = (data: FormattedDataPoint[], targetPoints = 100): FormattedDataPoint[] => {
 | 
				
			||||||
 | 
					  if (data.length <= targetPoints) return data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const result: FormattedDataPoint[] = [];
 | 
				
			||||||
 | 
					  const step = Math.floor(data.length / targetPoints);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  for (let i = 0; i < data.length; i += step) {
 | 
				
			||||||
 | 
					    // Calculate average for this segment
 | 
				
			||||||
 | 
					    let sum = 0;
 | 
				
			||||||
 | 
					    let count = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (let j = i; j < i + step && j < data.length; j++) {
 | 
				
			||||||
 | 
					      sum += data[j].value;
 | 
				
			||||||
 | 
					      count++;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Use the first timestamp in this segment but the average value
 | 
				
			||||||
 | 
					    if (count > 0) {
 | 
				
			||||||
 | 
					      result.push({
 | 
				
			||||||
 | 
					        ...data[i],
 | 
				
			||||||
 | 
					        value: sum / count
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Always include the last point for completeness
 | 
				
			||||||
 | 
					  if (data.length > 0 && (result.length === 0 || result[result.length - 1].timestamp !== data[data.length - 1].timestamp)) {
 | 
				
			||||||
 | 
					    result.push(data[data.length - 1]);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return result;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function FeedChart({ data }: FeedChartProps) {
 | 
				
			||||||
 | 
					  // Format and downsample data for the chart
 | 
				
			||||||
 | 
					  const formattedData = useMemo(() => {
 | 
				
			||||||
 | 
					    const rawFormatted = data.map((point) => ({
 | 
				
			||||||
 | 
					      timestamp: new Date(point.timestamp).getTime(),
 | 
				
			||||||
 | 
					      value: parseFloat(point.data),
 | 
				
			||||||
 | 
					      formattedTime: format(new Date(point.timestamp), "MMM dd, yyyy HH:mm:ss"),
 | 
				
			||||||
 | 
					    }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Sort by timestamp to ensure proper line connection
 | 
				
			||||||
 | 
					    rawFormatted.sort((a, b) => a.timestamp - b.timestamp);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Downsample to reduce visual clutter
 | 
				
			||||||
 | 
					    return downsampleData(rawFormatted);
 | 
				
			||||||
 | 
					  }, [data]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Custom tooltip
 | 
				
			||||||
 | 
					  const CustomTooltip = ({ active, payload }: TooltipProps<number, string>) => {
 | 
				
			||||||
 | 
					    if (active && payload && payload.length) {
 | 
				
			||||||
 | 
					      const data = payload[0].payload as FormattedDataPoint;
 | 
				
			||||||
 | 
					      return (
 | 
				
			||||||
 | 
					        <div className="bg-background border rounded-md shadow-md p-2 text-sm">
 | 
				
			||||||
 | 
					          <p className="font-medium">{data.formattedTime}</p>
 | 
				
			||||||
 | 
					          <p className="text-primary">Value: {data.value.toFixed(2)}</p>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className="w-full h-[350px]">
 | 
				
			||||||
 | 
					      <ResponsiveContainer width="100%" height="100%">
 | 
				
			||||||
 | 
					        <LineChart
 | 
				
			||||||
 | 
					          data={formattedData}
 | 
				
			||||||
 | 
					          margin={{ top: 10, right: 30, left: 10, bottom: 5 }}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <CartesianGrid strokeDasharray="3 3" stroke="var(--border)" opacity={0.5} />
 | 
				
			||||||
 | 
					          <XAxis
 | 
				
			||||||
 | 
					            dataKey="timestamp"
 | 
				
			||||||
 | 
					            type="number"
 | 
				
			||||||
 | 
					            domain={["auto", "auto"]}
 | 
				
			||||||
 | 
					            name="Time"
 | 
				
			||||||
 | 
					            tickFormatter={(unixTime) => format(new Date(unixTime), "HH:mm:ss")}
 | 
				
			||||||
 | 
					            tickCount={7}
 | 
				
			||||||
 | 
					            stroke="var(--muted-foreground)"
 | 
				
			||||||
 | 
					            fontSize={12}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					          <YAxis
 | 
				
			||||||
 | 
					            stroke="var(--muted-foreground)"
 | 
				
			||||||
 | 
					            tickFormatter={(value) => value.toString()}
 | 
				
			||||||
 | 
					            fontSize={12}
 | 
				
			||||||
 | 
					            width={50}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					          <Tooltip content={<CustomTooltip />} />
 | 
				
			||||||
 | 
					          <Line
 | 
				
			||||||
 | 
					            type="linear"
 | 
				
			||||||
 | 
					            dataKey="value"
 | 
				
			||||||
 | 
					            stroke="hsl(221.2, 83.2%, 53.3%)"
 | 
				
			||||||
 | 
					            strokeWidth={2}
 | 
				
			||||||
 | 
					            dot={false}
 | 
				
			||||||
 | 
					            activeDot={{ r: 5, strokeWidth: 1, stroke: "hsl(221.2, 83.2%, 53.3%)", fill: "var(--background)" }}
 | 
				
			||||||
 | 
					            isAnimationActive={true}
 | 
				
			||||||
 | 
					            animationDuration={500}
 | 
				
			||||||
 | 
					            connectNulls={true}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </LineChart>
 | 
				
			||||||
 | 
					      </ResponsiveContainer>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										187
									
								
								src/components/Header.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										187
									
								
								src/components/Header.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,187 @@
 | 
				
			|||||||
 | 
					import { Link, useNavigate } from "react-router-dom";
 | 
				
			||||||
 | 
					import { Button } from "@/components/ui/button";
 | 
				
			||||||
 | 
					import { ThemeToggle } from "@/components/ThemeToggle";
 | 
				
			||||||
 | 
					import { isAuthenticated, clearToken, getUsername } from "@/lib/auth";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  DropdownMenu,
 | 
				
			||||||
 | 
					  DropdownMenuContent,
 | 
				
			||||||
 | 
					  DropdownMenuItem,
 | 
				
			||||||
 | 
					  DropdownMenuLabel,
 | 
				
			||||||
 | 
					  DropdownMenuSeparator,
 | 
				
			||||||
 | 
					  DropdownMenuTrigger,
 | 
				
			||||||
 | 
					} from "@/components/ui/dropdown-menu";
 | 
				
			||||||
 | 
					import { Avatar, AvatarFallback } from "@/components/ui/avatar";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  ActivitySquare,
 | 
				
			||||||
 | 
					  ChevronDown,
 | 
				
			||||||
 | 
					  ExternalLink,
 | 
				
			||||||
 | 
					  LogOut,
 | 
				
			||||||
 | 
					  Menu,
 | 
				
			||||||
 | 
					  Settings,
 | 
				
			||||||
 | 
					  UserCircle,
 | 
				
			||||||
 | 
					} from "lucide-react";
 | 
				
			||||||
 | 
					import { toast } from "sonner";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function Header() {
 | 
				
			||||||
 | 
					  const navigate = useNavigate();
 | 
				
			||||||
 | 
					  const authenticated = isAuthenticated();
 | 
				
			||||||
 | 
					  const username = getUsername();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleLogout = () => {
 | 
				
			||||||
 | 
					    clearToken();
 | 
				
			||||||
 | 
					    toast.success("You have been logged out");
 | 
				
			||||||
 | 
					    navigate("/login");
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const getUserInitials = () => {
 | 
				
			||||||
 | 
					    if (!username) return "U";
 | 
				
			||||||
 | 
					    return username.substring(0, 2).toUpperCase();
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <header>
 | 
				
			||||||
 | 
					      {/* Alpha Disclaimer */}
 | 
				
			||||||
 | 
					      <div className="bg-red-500 text-white text-center py-1 text-sm">
 | 
				
			||||||
 | 
					        Alpha Version – Report any issues to{" "}
 | 
				
			||||||
 | 
					        <a href="mailto:me@mahdium.ir" className="underline">
 | 
				
			||||||
 | 
					          me@mahdium.ir
 | 
				
			||||||
 | 
					        </a>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div className="sticky top-0 z-50 w-full border-b border-border/40 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
 | 
				
			||||||
 | 
					        <div className="container flex h-16 items-center justify-between">
 | 
				
			||||||
 | 
					          {/* Logo and title */}
 | 
				
			||||||
 | 
					          <div className="flex items-center gap-2">
 | 
				
			||||||
 | 
					            <Link to="/" className="flex items-center space-x-2">
 | 
				
			||||||
 | 
					              <ActivitySquare
 | 
				
			||||||
 | 
					                className="h-6 w-6"
 | 
				
			||||||
 | 
					                style={{ color: "hsl(221.2, 83.2%, 53.3%)" }}
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					              <span className="text-xl font-bold">IoT Plotter</span>
 | 
				
			||||||
 | 
					            </Link>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          {/* Desktop view */}
 | 
				
			||||||
 | 
					          <div className="hidden md:flex items-center gap-4">
 | 
				
			||||||
 | 
					            <Button
 | 
				
			||||||
 | 
					              variant="default"
 | 
				
			||||||
 | 
					              className="font-semibold bg-blue-600 text-white hover:bg-blue-700"
 | 
				
			||||||
 | 
					              onClick={() => (window.location.href = "mailto:me@mahdium.ir")}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              Contact
 | 
				
			||||||
 | 
					            </Button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <Button
 | 
				
			||||||
 | 
					              variant="outline"
 | 
				
			||||||
 | 
					              onClick={() =>
 | 
				
			||||||
 | 
					                window.open("https://io-doc.monasefloadbalancer.ir", "_blank")
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <ExternalLink className="mr-2 h-6 w-6" />
 | 
				
			||||||
 | 
					              API Documentation
 | 
				
			||||||
 | 
					            </Button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <ThemeToggle />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            {authenticated ? (
 | 
				
			||||||
 | 
					              <DropdownMenu>
 | 
				
			||||||
 | 
					                <DropdownMenuTrigger asChild>
 | 
				
			||||||
 | 
					                  <Button variant="ghost" className="flex items-center gap-2">
 | 
				
			||||||
 | 
					                    <Avatar className="h-8 w-8">
 | 
				
			||||||
 | 
					                      <AvatarFallback>{getUserInitials()}</AvatarFallback>
 | 
				
			||||||
 | 
					                    </Avatar>
 | 
				
			||||||
 | 
					                    <span className="hidden sm:inline-block">{username}</span>
 | 
				
			||||||
 | 
					                    <ChevronDown className="h-4 w-4 opacity-50" />
 | 
				
			||||||
 | 
					                  </Button>
 | 
				
			||||||
 | 
					                </DropdownMenuTrigger>
 | 
				
			||||||
 | 
					                <DropdownMenuContent align="end" className="w-56">
 | 
				
			||||||
 | 
					                  <DropdownMenuLabel>My Account</DropdownMenuLabel>
 | 
				
			||||||
 | 
					                  <DropdownMenuSeparator />
 | 
				
			||||||
 | 
					                  <DropdownMenuItem onClick={() => navigate("/dashboard")}>
 | 
				
			||||||
 | 
					                    <ActivitySquare className="mr-2 h-4 w-4" />
 | 
				
			||||||
 | 
					                    <span>Dashboard</span>
 | 
				
			||||||
 | 
					                  </DropdownMenuItem>
 | 
				
			||||||
 | 
					                  <DropdownMenuItem onClick={() => navigate("/api-keys")}>
 | 
				
			||||||
 | 
					                    <Settings className="mr-2 h-4 w-4" />
 | 
				
			||||||
 | 
					                    <span>API Keys</span>
 | 
				
			||||||
 | 
					                  </DropdownMenuItem>
 | 
				
			||||||
 | 
					                  <DropdownMenuItem onClick={() => navigate("/profile")}>
 | 
				
			||||||
 | 
					                    <UserCircle className="mr-2 h-4 w-4" />
 | 
				
			||||||
 | 
					                    <span>Profile</span>
 | 
				
			||||||
 | 
					                  </DropdownMenuItem>
 | 
				
			||||||
 | 
					                  <DropdownMenuSeparator />
 | 
				
			||||||
 | 
					                  <DropdownMenuItem onClick={handleLogout}>
 | 
				
			||||||
 | 
					                    <LogOut className="mr-2 h-4 w-4" />
 | 
				
			||||||
 | 
					                    <span>Logout</span>
 | 
				
			||||||
 | 
					                  </DropdownMenuItem>
 | 
				
			||||||
 | 
					                </DropdownMenuContent>
 | 
				
			||||||
 | 
					              </DropdownMenu>
 | 
				
			||||||
 | 
					            ) : (
 | 
				
			||||||
 | 
					              <div className="flex items-center gap-2">
 | 
				
			||||||
 | 
					                <Button variant="ghost" onClick={() => navigate("/login")}>
 | 
				
			||||||
 | 
					                  Login
 | 
				
			||||||
 | 
					                </Button>
 | 
				
			||||||
 | 
					                <Button onClick={() => navigate("/register")}>Sign Up</Button>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          {/* Mobile view */}
 | 
				
			||||||
 | 
					          <div className="md:hidden">
 | 
				
			||||||
 | 
					            <DropdownMenu>
 | 
				
			||||||
 | 
					              <DropdownMenuTrigger asChild>
 | 
				
			||||||
 | 
					                <Button variant="ghost" className="p-2">
 | 
				
			||||||
 | 
					                  <Menu className="h-6 w-6" />
 | 
				
			||||||
 | 
					                </Button>
 | 
				
			||||||
 | 
					              </DropdownMenuTrigger>
 | 
				
			||||||
 | 
					              <DropdownMenuContent align="end" className="w-56">
 | 
				
			||||||
 | 
					                <DropdownMenuItem
 | 
				
			||||||
 | 
					                  onClick={() => (window.location.href = "mailto:me@mahdium.ir")}
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  Contact
 | 
				
			||||||
 | 
					                </DropdownMenuItem>
 | 
				
			||||||
 | 
					                <DropdownMenuItem
 | 
				
			||||||
 | 
					                  onClick={() =>
 | 
				
			||||||
 | 
					                    window.open("https://io-doc.monasefloadbalancer.ir", "_blank")
 | 
				
			||||||
 | 
					                  }
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  API Documentation
 | 
				
			||||||
 | 
					                </DropdownMenuItem>
 | 
				
			||||||
 | 
					                <DropdownMenuItem>
 | 
				
			||||||
 | 
					                  <ThemeToggle />
 | 
				
			||||||
 | 
					                </DropdownMenuItem>
 | 
				
			||||||
 | 
					                <DropdownMenuSeparator />
 | 
				
			||||||
 | 
					                {authenticated ? (
 | 
				
			||||||
 | 
					                  <>
 | 
				
			||||||
 | 
					                    <DropdownMenuItem onClick={() => navigate("/dashboard")}>
 | 
				
			||||||
 | 
					                      Dashboard
 | 
				
			||||||
 | 
					                    </DropdownMenuItem>
 | 
				
			||||||
 | 
					                    <DropdownMenuItem onClick={() => navigate("/api-keys")}>
 | 
				
			||||||
 | 
					                      API Keys
 | 
				
			||||||
 | 
					                    </DropdownMenuItem>
 | 
				
			||||||
 | 
					                    <DropdownMenuItem onClick={() => navigate("/profile")}>
 | 
				
			||||||
 | 
					                      Profile
 | 
				
			||||||
 | 
					                    </DropdownMenuItem>
 | 
				
			||||||
 | 
					                    <DropdownMenuSeparator />
 | 
				
			||||||
 | 
					                    <DropdownMenuItem onClick={handleLogout}>
 | 
				
			||||||
 | 
					                      Logout
 | 
				
			||||||
 | 
					                    </DropdownMenuItem>
 | 
				
			||||||
 | 
					                  </>
 | 
				
			||||||
 | 
					                ) : (
 | 
				
			||||||
 | 
					                  <>
 | 
				
			||||||
 | 
					                    <DropdownMenuItem onClick={() => navigate("/login")}>
 | 
				
			||||||
 | 
					                      Login
 | 
				
			||||||
 | 
					                    </DropdownMenuItem>
 | 
				
			||||||
 | 
					                    <DropdownMenuItem onClick={() => navigate("/register")}>
 | 
				
			||||||
 | 
					                      Sign Up
 | 
				
			||||||
 | 
					                    </DropdownMenuItem>
 | 
				
			||||||
 | 
					                  </>
 | 
				
			||||||
 | 
					                )}
 | 
				
			||||||
 | 
					              </DropdownMenuContent>
 | 
				
			||||||
 | 
					            </DropdownMenu>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </header>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										31
									
								
								src/components/Layout.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/components/Layout.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,31 @@
 | 
				
			|||||||
 | 
					import { Outlet } from "react-router-dom";
 | 
				
			||||||
 | 
					import { Header } from "@/components/Header";
 | 
				
			||||||
 | 
					import { Toaster } from "sonner";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function Layout() {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className="min-h-screen flex flex-col">
 | 
				
			||||||
 | 
					      <Header />
 | 
				
			||||||
 | 
					      <main className="flex-1 container py-6">
 | 
				
			||||||
 | 
					        <Outlet />
 | 
				
			||||||
 | 
					      </main>
 | 
				
			||||||
 | 
					      <footer className="border-t py-4">
 | 
				
			||||||
 | 
					        <div className="container flex items-center justify-between text-sm text-muted-foreground">
 | 
				
			||||||
 | 
					          <p>
 | 
				
			||||||
 | 
					            IoT Plotter © {new Date().getFullYear()} -{" "}
 | 
				
			||||||
 | 
					            <a
 | 
				
			||||||
 | 
					              href="https://mahdium.ir"
 | 
				
			||||||
 | 
					              target="_blank"
 | 
				
			||||||
 | 
					              rel="noopener noreferrer"
 | 
				
			||||||
 | 
					              className="underline"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              mahdium.ir
 | 
				
			||||||
 | 
					            </a>
 | 
				
			||||||
 | 
					          </p>
 | 
				
			||||||
 | 
					          <p>Version 0.0.3 - Alpha</p>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </footer>
 | 
				
			||||||
 | 
					      <Toaster position="top-right" />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										21
									
								
								src/components/ProtectedRoute.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/components/ProtectedRoute.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					import { ReactNode, useEffect } from "react";
 | 
				
			||||||
 | 
					import { useNavigate } from "react-router-dom";
 | 
				
			||||||
 | 
					import { isAuthenticated } from "@/lib/auth";
 | 
				
			||||||
 | 
					import { toast } from "sonner";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface ProtectedRouteProps {
 | 
				
			||||||
 | 
					  children: ReactNode;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function ProtectedRoute({ children }: ProtectedRouteProps) {
 | 
				
			||||||
 | 
					  const navigate = useNavigate();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (!isAuthenticated()) {
 | 
				
			||||||
 | 
					      toast.error("Please login to access this page");
 | 
				
			||||||
 | 
					      navigate("/login");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [navigate]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return <>{children}</>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										36
									
								
								src/components/ThemeToggle.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								src/components/ThemeToggle.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,36 @@
 | 
				
			|||||||
 | 
					import { Moon, Sun } from "lucide-react";
 | 
				
			||||||
 | 
					import { useTheme } from "@/lib/theme-provider";
 | 
				
			||||||
 | 
					import { Button } from "@/components/ui/button";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  DropdownMenu,
 | 
				
			||||||
 | 
					  DropdownMenuContent,
 | 
				
			||||||
 | 
					  DropdownMenuItem,
 | 
				
			||||||
 | 
					  DropdownMenuTrigger,
 | 
				
			||||||
 | 
					} from "@/components/ui/dropdown-menu";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function ThemeToggle() {
 | 
				
			||||||
 | 
					  const { theme, setTheme } = useTheme();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <DropdownMenu>
 | 
				
			||||||
 | 
					      <DropdownMenuTrigger asChild>
 | 
				
			||||||
 | 
					        <Button variant="outline" size="icon" className="h-9 w-9">
 | 
				
			||||||
 | 
					          <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
 | 
				
			||||||
 | 
					          <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
 | 
				
			||||||
 | 
					          <span className="sr-only">Toggle theme</span>
 | 
				
			||||||
 | 
					        </Button>
 | 
				
			||||||
 | 
					      </DropdownMenuTrigger>
 | 
				
			||||||
 | 
					      <DropdownMenuContent align="end">
 | 
				
			||||||
 | 
					        <DropdownMenuItem onClick={() => setTheme("light")}>
 | 
				
			||||||
 | 
					          Light
 | 
				
			||||||
 | 
					        </DropdownMenuItem>
 | 
				
			||||||
 | 
					        <DropdownMenuItem onClick={() => setTheme("dark")}>
 | 
				
			||||||
 | 
					          Dark
 | 
				
			||||||
 | 
					        </DropdownMenuItem>
 | 
				
			||||||
 | 
					        <DropdownMenuItem onClick={() => setTheme("system")}>
 | 
				
			||||||
 | 
					          System
 | 
				
			||||||
 | 
					        </DropdownMenuItem>
 | 
				
			||||||
 | 
					      </DropdownMenuContent>
 | 
				
			||||||
 | 
					    </DropdownMenu>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										59
									
								
								src/components/ui/alert.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								src/components/ui/alert.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,59 @@
 | 
				
			|||||||
 | 
					import * as React from "react"
 | 
				
			||||||
 | 
					import { cva, type VariantProps } from "class-variance-authority"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { cn } from "@/lib/utils"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const alertVariants = cva(
 | 
				
			||||||
 | 
					  "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    variants: {
 | 
				
			||||||
 | 
					      variant: {
 | 
				
			||||||
 | 
					        default: "bg-background text-foreground",
 | 
				
			||||||
 | 
					        destructive:
 | 
				
			||||||
 | 
					          "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    defaultVariants: {
 | 
				
			||||||
 | 
					      variant: "default",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Alert = React.forwardRef<
 | 
				
			||||||
 | 
					  HTMLDivElement,
 | 
				
			||||||
 | 
					  React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
 | 
				
			||||||
 | 
					>(({ className, variant, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <div
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    role="alert"
 | 
				
			||||||
 | 
					    className={cn(alertVariants({ variant }), className)}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					Alert.displayName = "Alert"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const AlertTitle = React.forwardRef<
 | 
				
			||||||
 | 
					  HTMLParagraphElement,
 | 
				
			||||||
 | 
					  React.HTMLAttributes<HTMLHeadingElement>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <h5
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn("mb-1 font-medium leading-none tracking-tight", className)}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					AlertTitle.displayName = "AlertTitle"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const AlertDescription = React.forwardRef<
 | 
				
			||||||
 | 
					  HTMLParagraphElement,
 | 
				
			||||||
 | 
					  React.HTMLAttributes<HTMLParagraphElement>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <div
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn("text-sm [&_p]:leading-relaxed", className)}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					AlertDescription.displayName = "AlertDescription"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export { Alert, AlertTitle, AlertDescription }
 | 
				
			||||||
							
								
								
									
										48
									
								
								src/components/ui/avatar.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								src/components/ui/avatar.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,48 @@
 | 
				
			|||||||
 | 
					import * as React from "react"
 | 
				
			||||||
 | 
					import * as AvatarPrimitive from "@radix-ui/react-avatar"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { cn } from "@/lib/utils"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Avatar = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof AvatarPrimitive.Root>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <AvatarPrimitive.Root
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn(
 | 
				
			||||||
 | 
					      "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
 | 
				
			||||||
 | 
					      className
 | 
				
			||||||
 | 
					    )}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					Avatar.displayName = AvatarPrimitive.Root.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const AvatarImage = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof AvatarPrimitive.Image>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <AvatarPrimitive.Image
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn("aspect-square h-full w-full", className)}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					AvatarImage.displayName = AvatarPrimitive.Image.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const AvatarFallback = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof AvatarPrimitive.Fallback>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <AvatarPrimitive.Fallback
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn(
 | 
				
			||||||
 | 
					      "flex h-full w-full items-center justify-center rounded-full bg-muted",
 | 
				
			||||||
 | 
					      className
 | 
				
			||||||
 | 
					    )}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export { Avatar, AvatarImage, AvatarFallback }
 | 
				
			||||||
							
								
								
									
										57
									
								
								src/components/ui/button.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								src/components/ui/button.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,57 @@
 | 
				
			|||||||
 | 
					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-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    variants: {
 | 
				
			||||||
 | 
					      variant: {
 | 
				
			||||||
 | 
					        default:
 | 
				
			||||||
 | 
					          "bg-primary text-primary-foreground shadow hover:bg-primary/90",
 | 
				
			||||||
 | 
					        destructive:
 | 
				
			||||||
 | 
					          "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
 | 
				
			||||||
 | 
					        outline:
 | 
				
			||||||
 | 
					          "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
 | 
				
			||||||
 | 
					        secondary:
 | 
				
			||||||
 | 
					          "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
 | 
				
			||||||
 | 
					        ghost: "hover:bg-accent hover:text-accent-foreground",
 | 
				
			||||||
 | 
					        link: "text-primary underline-offset-4 hover:underline",
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      size: {
 | 
				
			||||||
 | 
					        default: "h-9 px-4 py-2",
 | 
				
			||||||
 | 
					        sm: "h-8 rounded-md px-3 text-xs",
 | 
				
			||||||
 | 
					        lg: "h-10 rounded-md px-8",
 | 
				
			||||||
 | 
					        icon: "h-9 w-9",
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    defaultVariants: {
 | 
				
			||||||
 | 
					      variant: "default",
 | 
				
			||||||
 | 
					      size: "default",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface ButtonProps
 | 
				
			||||||
 | 
					  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
 | 
				
			||||||
 | 
					    VariantProps<typeof buttonVariants> {
 | 
				
			||||||
 | 
					  asChild?: boolean
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
 | 
				
			||||||
 | 
					  ({ className, variant, size, asChild = false, ...props }, ref) => {
 | 
				
			||||||
 | 
					    const Comp = asChild ? Slot : "button"
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <Comp
 | 
				
			||||||
 | 
					        className={cn(buttonVariants({ variant, size, className }))}
 | 
				
			||||||
 | 
					        ref={ref}
 | 
				
			||||||
 | 
					        {...props}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					Button.displayName = "Button"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export { Button, buttonVariants }
 | 
				
			||||||
							
								
								
									
										76
									
								
								src/components/ui/calendar.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								src/components/ui/calendar.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,76 @@
 | 
				
			|||||||
 | 
					"use client"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import * as React from "react"
 | 
				
			||||||
 | 
					import { ChevronLeft, ChevronRight } from "lucide-react"
 | 
				
			||||||
 | 
					import { DayPicker } from "react-day-picker"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { cn } from "@/lib/utils"
 | 
				
			||||||
 | 
					import { buttonVariants } from "@/components/ui/button"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type CalendarProps = React.ComponentProps<typeof DayPicker>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function Calendar({
 | 
				
			||||||
 | 
					  className,
 | 
				
			||||||
 | 
					  classNames,
 | 
				
			||||||
 | 
					  showOutsideDays = true,
 | 
				
			||||||
 | 
					  ...props
 | 
				
			||||||
 | 
					}: CalendarProps) {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <DayPicker
 | 
				
			||||||
 | 
					      showOutsideDays={showOutsideDays}
 | 
				
			||||||
 | 
					      className={cn("p-3", className)}
 | 
				
			||||||
 | 
					      classNames={{
 | 
				
			||||||
 | 
					        months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
 | 
				
			||||||
 | 
					        month: "space-y-4",
 | 
				
			||||||
 | 
					        caption: "flex justify-center pt-1 relative items-center",
 | 
				
			||||||
 | 
					        caption_label: "text-sm font-medium",
 | 
				
			||||||
 | 
					        nav: "space-x-1 flex items-center",
 | 
				
			||||||
 | 
					        nav_button: cn(
 | 
				
			||||||
 | 
					          buttonVariants({ variant: "outline" }),
 | 
				
			||||||
 | 
					          "h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        nav_button_previous: "absolute left-1",
 | 
				
			||||||
 | 
					        nav_button_next: "absolute right-1",
 | 
				
			||||||
 | 
					        table: "w-full border-collapse space-y-1",
 | 
				
			||||||
 | 
					        head_row: "flex",
 | 
				
			||||||
 | 
					        head_cell:
 | 
				
			||||||
 | 
					          "text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
 | 
				
			||||||
 | 
					        row: "flex w-full mt-2",
 | 
				
			||||||
 | 
					        cell: cn(
 | 
				
			||||||
 | 
					          "relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected].day-range-end)]:rounded-r-md",
 | 
				
			||||||
 | 
					          props.mode === "range"
 | 
				
			||||||
 | 
					            ? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
 | 
				
			||||||
 | 
					            : "[&:has([aria-selected])]:rounded-md"
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        day: cn(
 | 
				
			||||||
 | 
					          buttonVariants({ variant: "ghost" }),
 | 
				
			||||||
 | 
					          "h-8 w-8 p-0 font-normal aria-selected:opacity-100"
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        day_range_start: "day-range-start",
 | 
				
			||||||
 | 
					        day_range_end: "day-range-end",
 | 
				
			||||||
 | 
					        day_selected:
 | 
				
			||||||
 | 
					          "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
 | 
				
			||||||
 | 
					        day_today: "bg-accent text-accent-foreground",
 | 
				
			||||||
 | 
					        day_outside:
 | 
				
			||||||
 | 
					          "day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground",
 | 
				
			||||||
 | 
					        day_disabled: "text-muted-foreground opacity-50",
 | 
				
			||||||
 | 
					        day_range_middle:
 | 
				
			||||||
 | 
					          "aria-selected:bg-accent aria-selected:text-accent-foreground",
 | 
				
			||||||
 | 
					        day_hidden: "invisible",
 | 
				
			||||||
 | 
					        ...classNames,
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
 | 
					      components={{
 | 
				
			||||||
 | 
					        IconLeft: ({ className, ...props }) => (
 | 
				
			||||||
 | 
					          <ChevronLeft className={cn("h-4 w-4", className)} {...props} />
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        IconRight: ({ className, ...props }) => (
 | 
				
			||||||
 | 
					          <ChevronRight className={cn("h-4 w-4", className)} {...props} />
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
 | 
					      {...props}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					Calendar.displayName = "Calendar"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export { Calendar }
 | 
				
			||||||
							
								
								
									
										76
									
								
								src/components/ui/card.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								src/components/ui/card.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,76 @@
 | 
				
			|||||||
 | 
					import * as React from "react"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { cn } from "@/lib/utils"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Card = React.forwardRef<
 | 
				
			||||||
 | 
					  HTMLDivElement,
 | 
				
			||||||
 | 
					  React.HTMLAttributes<HTMLDivElement>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <div
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn(
 | 
				
			||||||
 | 
					      "rounded-xl border bg-card text-card-foreground shadow",
 | 
				
			||||||
 | 
					      className
 | 
				
			||||||
 | 
					    )}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					Card.displayName = "Card"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const CardHeader = React.forwardRef<
 | 
				
			||||||
 | 
					  HTMLDivElement,
 | 
				
			||||||
 | 
					  React.HTMLAttributes<HTMLDivElement>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <div
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn("flex flex-col space-y-1.5 p-6", className)}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					CardHeader.displayName = "CardHeader"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const CardTitle = React.forwardRef<
 | 
				
			||||||
 | 
					  HTMLDivElement,
 | 
				
			||||||
 | 
					  React.HTMLAttributes<HTMLDivElement>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <div
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn("font-semibold leading-none tracking-tight", className)}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					CardTitle.displayName = "CardTitle"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const CardDescription = React.forwardRef<
 | 
				
			||||||
 | 
					  HTMLDivElement,
 | 
				
			||||||
 | 
					  React.HTMLAttributes<HTMLDivElement>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <div
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn("text-sm text-muted-foreground", className)}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					CardDescription.displayName = "CardDescription"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const CardContent = React.forwardRef<
 | 
				
			||||||
 | 
					  HTMLDivElement,
 | 
				
			||||||
 | 
					  React.HTMLAttributes<HTMLDivElement>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					CardContent.displayName = "CardContent"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const CardFooter = React.forwardRef<
 | 
				
			||||||
 | 
					  HTMLDivElement,
 | 
				
			||||||
 | 
					  React.HTMLAttributes<HTMLDivElement>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <div
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn("flex items-center p-6 pt-0", className)}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					CardFooter.displayName = "CardFooter"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
 | 
				
			||||||
							
								
								
									
										116
									
								
								src/components/ui/date-time-picker.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								src/components/ui/date-time-picker.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,116 @@
 | 
				
			|||||||
 | 
					import * as React from "react";
 | 
				
			||||||
 | 
					import { format } from "date-fns";
 | 
				
			||||||
 | 
					import { Calendar as CalendarIcon } from "lucide-react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { cn } from "@/lib/utils";
 | 
				
			||||||
 | 
					import { Button } from "@/components/ui/button";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  Popover,
 | 
				
			||||||
 | 
					  PopoverContent,
 | 
				
			||||||
 | 
					  PopoverTrigger,
 | 
				
			||||||
 | 
					} from "@/components/ui/popover";
 | 
				
			||||||
 | 
					import { Calendar } from "@/components/ui/calendar";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface DateTimePickerProps {
 | 
				
			||||||
 | 
					  date: Date;
 | 
				
			||||||
 | 
					  setDate: (date: Date) => void;
 | 
				
			||||||
 | 
					  disabled?: boolean;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function DateTimePicker({
 | 
				
			||||||
 | 
					  date,
 | 
				
			||||||
 | 
					  setDate,
 | 
				
			||||||
 | 
					  disabled = false,
 | 
				
			||||||
 | 
					}: DateTimePickerProps) {
 | 
				
			||||||
 | 
					  const [selectedDate, setSelectedDate] = React.useState<Date | undefined>(date);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Update the date when selectedDate changes
 | 
				
			||||||
 | 
					  React.useEffect(() => {
 | 
				
			||||||
 | 
					    if (selectedDate) {
 | 
				
			||||||
 | 
					      setDate(selectedDate);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [selectedDate, setDate]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Update the selectedDate when date prop changes
 | 
				
			||||||
 | 
					  React.useEffect(() => {
 | 
				
			||||||
 | 
					    setSelectedDate(date);
 | 
				
			||||||
 | 
					  }, [date]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleSelect = (selected: Date | undefined) => {
 | 
				
			||||||
 | 
					    if (selected) {
 | 
				
			||||||
 | 
					      const currentDate = selectedDate || new Date();
 | 
				
			||||||
 | 
					      const newDate = new Date(
 | 
				
			||||||
 | 
					        selected.getFullYear(),
 | 
				
			||||||
 | 
					        selected.getMonth(),
 | 
				
			||||||
 | 
					        selected.getDate(),
 | 
				
			||||||
 | 
					        currentDate.getHours(),
 | 
				
			||||||
 | 
					        currentDate.getMinutes(),
 | 
				
			||||||
 | 
					        currentDate.getSeconds()
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      setSelectedDate(newDate);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleTimeChange = (event: React.ChangeEvent<HTMLInputElement>) => {
 | 
				
			||||||
 | 
					    if (!selectedDate) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const [hours, minutes] = event.target.value.split(":").map(Number);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!isNaN(hours) && !isNaN(minutes)) {
 | 
				
			||||||
 | 
					      const newDate = new Date(selectedDate);
 | 
				
			||||||
 | 
					      newDate.setHours(hours);
 | 
				
			||||||
 | 
					      newDate.setMinutes(minutes);
 | 
				
			||||||
 | 
					      setSelectedDate(newDate);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const formattedDate = selectedDate
 | 
				
			||||||
 | 
					    ? format(selectedDate, "PPP")
 | 
				
			||||||
 | 
					    : "Pick a date";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const formattedTime = selectedDate
 | 
				
			||||||
 | 
					    ? format(selectedDate, "HH:mm")
 | 
				
			||||||
 | 
					    : "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className="flex flex-col space-y-2">
 | 
				
			||||||
 | 
					      <Popover>
 | 
				
			||||||
 | 
					        <PopoverTrigger asChild>
 | 
				
			||||||
 | 
					          <Button
 | 
				
			||||||
 | 
					            variant={"outline"}
 | 
				
			||||||
 | 
					            disabled={disabled}
 | 
				
			||||||
 | 
					            className={cn(
 | 
				
			||||||
 | 
					              "w-full justify-start text-left font-normal",
 | 
				
			||||||
 | 
					              !date && "text-muted-foreground"
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <CalendarIcon className="mr-2 h-4 w-4" />
 | 
				
			||||||
 | 
					            {formattedDate}
 | 
				
			||||||
 | 
					          </Button>
 | 
				
			||||||
 | 
					        </PopoverTrigger>
 | 
				
			||||||
 | 
					        <PopoverContent className="w-auto p-0">
 | 
				
			||||||
 | 
					          <Calendar
 | 
				
			||||||
 | 
					            mode="single"
 | 
				
			||||||
 | 
					            selected={selectedDate}
 | 
				
			||||||
 | 
					            onSelect={handleSelect}
 | 
				
			||||||
 | 
					            initialFocus
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </PopoverContent>
 | 
				
			||||||
 | 
					      </Popover>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <input
 | 
				
			||||||
 | 
					        type="time"
 | 
				
			||||||
 | 
					        className={cn(
 | 
				
			||||||
 | 
					          "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm",
 | 
				
			||||||
 | 
					          "ring-offset-background file:border-0 file:bg-transparent",
 | 
				
			||||||
 | 
					          "file:text-sm file:font-medium placeholder:text-muted-foreground",
 | 
				
			||||||
 | 
					          "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
 | 
				
			||||||
 | 
					          "focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					        value={formattedTime}
 | 
				
			||||||
 | 
					        onChange={handleTimeChange}
 | 
				
			||||||
 | 
					        disabled={disabled}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										120
									
								
								src/components/ui/dialog.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								src/components/ui/dialog.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,120 @@
 | 
				
			|||||||
 | 
					import * as React from "react"
 | 
				
			||||||
 | 
					import * as DialogPrimitive from "@radix-ui/react-dialog"
 | 
				
			||||||
 | 
					import { X } from "lucide-react"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { cn } from "@/lib/utils"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Dialog = DialogPrimitive.Root
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DialogTrigger = DialogPrimitive.Trigger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DialogPortal = DialogPrimitive.Portal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DialogClose = DialogPrimitive.Close
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DialogOverlay = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof DialogPrimitive.Overlay>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <DialogPrimitive.Overlay
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn(
 | 
				
			||||||
 | 
					      "fixed inset-0 z-50 bg-black/80  data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
 | 
				
			||||||
 | 
					      className
 | 
				
			||||||
 | 
					    )}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DialogContent = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof DialogPrimitive.Content>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
 | 
				
			||||||
 | 
					>(({ className, children, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <DialogPortal>
 | 
				
			||||||
 | 
					    <DialogOverlay />
 | 
				
			||||||
 | 
					    <DialogPrimitive.Content
 | 
				
			||||||
 | 
					      ref={ref}
 | 
				
			||||||
 | 
					      className={cn(
 | 
				
			||||||
 | 
					        "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 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 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
 | 
				
			||||||
 | 
					        className
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					      {...props}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      {children}
 | 
				
			||||||
 | 
					      <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
 | 
				
			||||||
 | 
					        <X className="h-4 w-4" />
 | 
				
			||||||
 | 
					        <span className="sr-only">Close</span>
 | 
				
			||||||
 | 
					      </DialogPrimitive.Close>
 | 
				
			||||||
 | 
					    </DialogPrimitive.Content>
 | 
				
			||||||
 | 
					  </DialogPortal>
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					DialogContent.displayName = DialogPrimitive.Content.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DialogHeader = ({
 | 
				
			||||||
 | 
					  className,
 | 
				
			||||||
 | 
					  ...props
 | 
				
			||||||
 | 
					}: React.HTMLAttributes<HTMLDivElement>) => (
 | 
				
			||||||
 | 
					  <div
 | 
				
			||||||
 | 
					    className={cn(
 | 
				
			||||||
 | 
					      "flex flex-col space-y-1.5 text-center sm:text-left",
 | 
				
			||||||
 | 
					      className
 | 
				
			||||||
 | 
					    )}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					DialogHeader.displayName = "DialogHeader"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DialogFooter = ({
 | 
				
			||||||
 | 
					  className,
 | 
				
			||||||
 | 
					  ...props
 | 
				
			||||||
 | 
					}: React.HTMLAttributes<HTMLDivElement>) => (
 | 
				
			||||||
 | 
					  <div
 | 
				
			||||||
 | 
					    className={cn(
 | 
				
			||||||
 | 
					      "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
 | 
				
			||||||
 | 
					      className
 | 
				
			||||||
 | 
					    )}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					DialogFooter.displayName = "DialogFooter"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DialogTitle = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof DialogPrimitive.Title>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <DialogPrimitive.Title
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn(
 | 
				
			||||||
 | 
					      "text-lg font-semibold leading-none tracking-tight",
 | 
				
			||||||
 | 
					      className
 | 
				
			||||||
 | 
					    )}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					DialogTitle.displayName = DialogPrimitive.Title.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DialogDescription = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof DialogPrimitive.Description>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <DialogPrimitive.Description
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn("text-sm text-muted-foreground", className)}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					DialogDescription.displayName = DialogPrimitive.Description.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export {
 | 
				
			||||||
 | 
					  Dialog,
 | 
				
			||||||
 | 
					  DialogPortal,
 | 
				
			||||||
 | 
					  DialogOverlay,
 | 
				
			||||||
 | 
					  DialogTrigger,
 | 
				
			||||||
 | 
					  DialogClose,
 | 
				
			||||||
 | 
					  DialogContent,
 | 
				
			||||||
 | 
					  DialogHeader,
 | 
				
			||||||
 | 
					  DialogFooter,
 | 
				
			||||||
 | 
					  DialogTitle,
 | 
				
			||||||
 | 
					  DialogDescription,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										201
									
								
								src/components/ui/dropdown-menu.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										201
									
								
								src/components/ui/dropdown-menu.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,201 @@
 | 
				
			|||||||
 | 
					"use client"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import * as React from "react"
 | 
				
			||||||
 | 
					import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
 | 
				
			||||||
 | 
					import { Check, ChevronRight, Circle } from "lucide-react"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { cn } from "@/lib/utils"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DropdownMenu = DropdownMenuPrimitive.Root
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DropdownMenuGroup = DropdownMenuPrimitive.Group
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DropdownMenuPortal = DropdownMenuPrimitive.Portal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DropdownMenuSub = DropdownMenuPrimitive.Sub
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DropdownMenuSubTrigger = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
 | 
				
			||||||
 | 
					    inset?: boolean
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					>(({ className, inset, children, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <DropdownMenuPrimitive.SubTrigger
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn(
 | 
				
			||||||
 | 
					      "flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
 | 
				
			||||||
 | 
					      inset && "pl-8",
 | 
				
			||||||
 | 
					      className
 | 
				
			||||||
 | 
					    )}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  >
 | 
				
			||||||
 | 
					    {children}
 | 
				
			||||||
 | 
					    <ChevronRight className="ml-auto" />
 | 
				
			||||||
 | 
					  </DropdownMenuPrimitive.SubTrigger>
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					DropdownMenuSubTrigger.displayName =
 | 
				
			||||||
 | 
					  DropdownMenuPrimitive.SubTrigger.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DropdownMenuSubContent = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <DropdownMenuPrimitive.SubContent
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn(
 | 
				
			||||||
 | 
					      "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg 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 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 origin-[--radix-dropdown-menu-content-transform-origin]",
 | 
				
			||||||
 | 
					      className
 | 
				
			||||||
 | 
					    )}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					DropdownMenuSubContent.displayName =
 | 
				
			||||||
 | 
					  DropdownMenuPrimitive.SubContent.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DropdownMenuContent = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof DropdownMenuPrimitive.Content>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
 | 
				
			||||||
 | 
					>(({ className, sideOffset = 4, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <DropdownMenuPrimitive.Portal>
 | 
				
			||||||
 | 
					    <DropdownMenuPrimitive.Content
 | 
				
			||||||
 | 
					      ref={ref}
 | 
				
			||||||
 | 
					      sideOffset={sideOffset}
 | 
				
			||||||
 | 
					      className={cn(
 | 
				
			||||||
 | 
					        "z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
 | 
				
			||||||
 | 
					        "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 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 origin-[--radix-dropdown-menu-content-transform-origin]",
 | 
				
			||||||
 | 
					        className
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					      {...props}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					  </DropdownMenuPrimitive.Portal>
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DropdownMenuItem = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof DropdownMenuPrimitive.Item>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
 | 
				
			||||||
 | 
					    inset?: boolean
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					>(({ className, inset, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <DropdownMenuPrimitive.Item
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn(
 | 
				
			||||||
 | 
					      "relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
 | 
				
			||||||
 | 
					      inset && "pl-8",
 | 
				
			||||||
 | 
					      className
 | 
				
			||||||
 | 
					    )}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DropdownMenuCheckboxItem = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
 | 
				
			||||||
 | 
					>(({ className, children, checked, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <DropdownMenuPrimitive.CheckboxItem
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn(
 | 
				
			||||||
 | 
					      "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
 | 
				
			||||||
 | 
					      className
 | 
				
			||||||
 | 
					    )}
 | 
				
			||||||
 | 
					    checked={checked}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  >
 | 
				
			||||||
 | 
					    <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
 | 
				
			||||||
 | 
					      <DropdownMenuPrimitive.ItemIndicator>
 | 
				
			||||||
 | 
					        <Check className="h-4 w-4" />
 | 
				
			||||||
 | 
					      </DropdownMenuPrimitive.ItemIndicator>
 | 
				
			||||||
 | 
					    </span>
 | 
				
			||||||
 | 
					    {children}
 | 
				
			||||||
 | 
					  </DropdownMenuPrimitive.CheckboxItem>
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					DropdownMenuCheckboxItem.displayName =
 | 
				
			||||||
 | 
					  DropdownMenuPrimitive.CheckboxItem.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DropdownMenuRadioItem = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
 | 
				
			||||||
 | 
					>(({ className, children, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <DropdownMenuPrimitive.RadioItem
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn(
 | 
				
			||||||
 | 
					      "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
 | 
				
			||||||
 | 
					      className
 | 
				
			||||||
 | 
					    )}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  >
 | 
				
			||||||
 | 
					    <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
 | 
				
			||||||
 | 
					      <DropdownMenuPrimitive.ItemIndicator>
 | 
				
			||||||
 | 
					        <Circle className="h-2 w-2 fill-current" />
 | 
				
			||||||
 | 
					      </DropdownMenuPrimitive.ItemIndicator>
 | 
				
			||||||
 | 
					    </span>
 | 
				
			||||||
 | 
					    {children}
 | 
				
			||||||
 | 
					  </DropdownMenuPrimitive.RadioItem>
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DropdownMenuLabel = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof DropdownMenuPrimitive.Label>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
 | 
				
			||||||
 | 
					    inset?: boolean
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					>(({ className, inset, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <DropdownMenuPrimitive.Label
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn(
 | 
				
			||||||
 | 
					      "px-2 py-1.5 text-sm font-semibold",
 | 
				
			||||||
 | 
					      inset && "pl-8",
 | 
				
			||||||
 | 
					      className
 | 
				
			||||||
 | 
					    )}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DropdownMenuSeparator = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <DropdownMenuPrimitive.Separator
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn("-mx-1 my-1 h-px bg-muted", className)}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DropdownMenuShortcut = ({
 | 
				
			||||||
 | 
					  className,
 | 
				
			||||||
 | 
					  ...props
 | 
				
			||||||
 | 
					}: React.HTMLAttributes<HTMLSpanElement>) => {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <span
 | 
				
			||||||
 | 
					      className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
 | 
				
			||||||
 | 
					      {...props}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export {
 | 
				
			||||||
 | 
					  DropdownMenu,
 | 
				
			||||||
 | 
					  DropdownMenuTrigger,
 | 
				
			||||||
 | 
					  DropdownMenuContent,
 | 
				
			||||||
 | 
					  DropdownMenuItem,
 | 
				
			||||||
 | 
					  DropdownMenuCheckboxItem,
 | 
				
			||||||
 | 
					  DropdownMenuRadioItem,
 | 
				
			||||||
 | 
					  DropdownMenuLabel,
 | 
				
			||||||
 | 
					  DropdownMenuSeparator,
 | 
				
			||||||
 | 
					  DropdownMenuShortcut,
 | 
				
			||||||
 | 
					  DropdownMenuGroup,
 | 
				
			||||||
 | 
					  DropdownMenuPortal,
 | 
				
			||||||
 | 
					  DropdownMenuSub,
 | 
				
			||||||
 | 
					  DropdownMenuSubContent,
 | 
				
			||||||
 | 
					  DropdownMenuSubTrigger,
 | 
				
			||||||
 | 
					  DropdownMenuRadioGroup,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										176
									
								
								src/components/ui/form.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										176
									
								
								src/components/ui/form.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,176 @@
 | 
				
			|||||||
 | 
					import * as React from "react"
 | 
				
			||||||
 | 
					import * as LabelPrimitive from "@radix-ui/react-label"
 | 
				
			||||||
 | 
					import { Slot } from "@radix-ui/react-slot"
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  Controller,
 | 
				
			||||||
 | 
					  FormProvider,
 | 
				
			||||||
 | 
					  useFormContext,
 | 
				
			||||||
 | 
					  type ControllerProps,
 | 
				
			||||||
 | 
					  type FieldPath,
 | 
				
			||||||
 | 
					  type FieldValues,
 | 
				
			||||||
 | 
					} from "react-hook-form"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { cn } from "@/lib/utils"
 | 
				
			||||||
 | 
					import { Label } from "@/components/ui/label"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Form = FormProvider
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface FormFieldContextValue<
 | 
				
			||||||
 | 
					  TFieldValues extends FieldValues = FieldValues,
 | 
				
			||||||
 | 
					  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
 | 
				
			||||||
 | 
					> {
 | 
				
			||||||
 | 
					  name: TName
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const FormFieldContext = React.createContext<FormFieldContextValue>(
 | 
				
			||||||
 | 
					  {} as FormFieldContextValue
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const FormField = <
 | 
				
			||||||
 | 
					  TFieldValues extends FieldValues = FieldValues,
 | 
				
			||||||
 | 
					  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
 | 
				
			||||||
 | 
					>({
 | 
				
			||||||
 | 
					  ...props
 | 
				
			||||||
 | 
					}: ControllerProps<TFieldValues, TName>) => {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <FormFieldContext.Provider value={{ name: props.name }}>
 | 
				
			||||||
 | 
					      <Controller {...props} />
 | 
				
			||||||
 | 
					    </FormFieldContext.Provider>
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const useFormField = () => {
 | 
				
			||||||
 | 
					  const fieldContext = React.useContext(FormFieldContext)
 | 
				
			||||||
 | 
					  const itemContext = React.useContext(FormItemContext)
 | 
				
			||||||
 | 
					  const { getFieldState, formState } = useFormContext()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const fieldState = getFieldState(fieldContext.name, formState)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!fieldContext) {
 | 
				
			||||||
 | 
					    throw new Error("useFormField should be used within <FormField>")
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { id } = itemContext
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    id,
 | 
				
			||||||
 | 
					    name: fieldContext.name,
 | 
				
			||||||
 | 
					    formItemId: `${id}-form-item`,
 | 
				
			||||||
 | 
					    formDescriptionId: `${id}-form-item-description`,
 | 
				
			||||||
 | 
					    formMessageId: `${id}-form-item-message`,
 | 
				
			||||||
 | 
					    ...fieldState,
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface FormItemContextValue {
 | 
				
			||||||
 | 
					  id: string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const FormItemContext = React.createContext<FormItemContextValue>(
 | 
				
			||||||
 | 
					  {} as FormItemContextValue
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const FormItem = React.forwardRef<
 | 
				
			||||||
 | 
					  HTMLDivElement,
 | 
				
			||||||
 | 
					  React.HTMLAttributes<HTMLDivElement>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => {
 | 
				
			||||||
 | 
					  const id = React.useId()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <FormItemContext.Provider value={{ id }}>
 | 
				
			||||||
 | 
					      <div ref={ref} className={cn("space-y-2", className)} {...props} />
 | 
				
			||||||
 | 
					    </FormItemContext.Provider>
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					FormItem.displayName = "FormItem"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const FormLabel = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof LabelPrimitive.Root>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => {
 | 
				
			||||||
 | 
					  const { error, formItemId } = useFormField()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Label
 | 
				
			||||||
 | 
					      ref={ref}
 | 
				
			||||||
 | 
					      className={cn(error && "text-destructive", className)}
 | 
				
			||||||
 | 
					      htmlFor={formItemId}
 | 
				
			||||||
 | 
					      {...props}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					FormLabel.displayName = "FormLabel"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const FormControl = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof Slot>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof Slot>
 | 
				
			||||||
 | 
					>(({ ...props }, ref) => {
 | 
				
			||||||
 | 
					  const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Slot
 | 
				
			||||||
 | 
					      ref={ref}
 | 
				
			||||||
 | 
					      id={formItemId}
 | 
				
			||||||
 | 
					      aria-describedby={
 | 
				
			||||||
 | 
					        !error
 | 
				
			||||||
 | 
					          ? `${formDescriptionId}`
 | 
				
			||||||
 | 
					          : `${formDescriptionId} ${formMessageId}`
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      aria-invalid={!!error}
 | 
				
			||||||
 | 
					      {...props}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					FormControl.displayName = "FormControl"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const FormDescription = React.forwardRef<
 | 
				
			||||||
 | 
					  HTMLParagraphElement,
 | 
				
			||||||
 | 
					  React.HTMLAttributes<HTMLParagraphElement>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => {
 | 
				
			||||||
 | 
					  const { formDescriptionId } = useFormField()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <p
 | 
				
			||||||
 | 
					      ref={ref}
 | 
				
			||||||
 | 
					      id={formDescriptionId}
 | 
				
			||||||
 | 
					      className={cn("text-[0.8rem] text-muted-foreground", className)}
 | 
				
			||||||
 | 
					      {...props}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					FormDescription.displayName = "FormDescription"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const FormMessage = React.forwardRef<
 | 
				
			||||||
 | 
					  HTMLParagraphElement,
 | 
				
			||||||
 | 
					  React.HTMLAttributes<HTMLParagraphElement>
 | 
				
			||||||
 | 
					>(({ className, children, ...props }, ref) => {
 | 
				
			||||||
 | 
					  const { error, formMessageId } = useFormField()
 | 
				
			||||||
 | 
					  const body = error ? String(error?.message ?? "") : children
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!body) {
 | 
				
			||||||
 | 
					    return null
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <p
 | 
				
			||||||
 | 
					      ref={ref}
 | 
				
			||||||
 | 
					      id={formMessageId}
 | 
				
			||||||
 | 
					      className={cn("text-[0.8rem] font-medium text-destructive", className)}
 | 
				
			||||||
 | 
					      {...props}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      {body}
 | 
				
			||||||
 | 
					    </p>
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					FormMessage.displayName = "FormMessage"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export {
 | 
				
			||||||
 | 
					  useFormField,
 | 
				
			||||||
 | 
					  Form,
 | 
				
			||||||
 | 
					  FormItem,
 | 
				
			||||||
 | 
					  FormLabel,
 | 
				
			||||||
 | 
					  FormControl,
 | 
				
			||||||
 | 
					  FormDescription,
 | 
				
			||||||
 | 
					  FormMessage,
 | 
				
			||||||
 | 
					  FormField,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										22
									
								
								src/components/ui/input.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/components/ui/input.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
				
			|||||||
 | 
					import * as React from "react"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { cn } from "@/lib/utils"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
 | 
				
			||||||
 | 
					  ({ className, type, ...props }, ref) => {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <input
 | 
				
			||||||
 | 
					        type={type}
 | 
				
			||||||
 | 
					        className={cn(
 | 
				
			||||||
 | 
					          "flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
 | 
				
			||||||
 | 
					          className
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					        ref={ref}
 | 
				
			||||||
 | 
					        {...props}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					Input.displayName = "Input"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export { Input }
 | 
				
			||||||
							
								
								
									
										26
									
								
								src/components/ui/label.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/components/ui/label.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
				
			|||||||
 | 
					"use client"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import * as React from "react"
 | 
				
			||||||
 | 
					import * as LabelPrimitive from "@radix-ui/react-label"
 | 
				
			||||||
 | 
					import { cva, type VariantProps } from "class-variance-authority"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { cn } from "@/lib/utils"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const labelVariants = cva(
 | 
				
			||||||
 | 
					  "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Label = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof LabelPrimitive.Root>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
 | 
				
			||||||
 | 
					    VariantProps<typeof labelVariants>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <LabelPrimitive.Root
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn(labelVariants(), className)}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					Label.displayName = LabelPrimitive.Root.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export { Label }
 | 
				
			||||||
							
								
								
									
										31
									
								
								src/components/ui/popover.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/components/ui/popover.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,31 @@
 | 
				
			|||||||
 | 
					import * as React from "react"
 | 
				
			||||||
 | 
					import * as PopoverPrimitive from "@radix-ui/react-popover"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { cn } from "@/lib/utils"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Popover = PopoverPrimitive.Root
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const PopoverTrigger = PopoverPrimitive.Trigger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const PopoverAnchor = PopoverPrimitive.Anchor
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const PopoverContent = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof PopoverPrimitive.Content>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
 | 
				
			||||||
 | 
					>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <PopoverPrimitive.Portal>
 | 
				
			||||||
 | 
					    <PopoverPrimitive.Content
 | 
				
			||||||
 | 
					      ref={ref}
 | 
				
			||||||
 | 
					      align={align}
 | 
				
			||||||
 | 
					      sideOffset={sideOffset}
 | 
				
			||||||
 | 
					      className={cn(
 | 
				
			||||||
 | 
					        "z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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 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 origin-[--radix-popover-content-transform-origin]",
 | 
				
			||||||
 | 
					        className
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					      {...props}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					  </PopoverPrimitive.Portal>
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					PopoverContent.displayName = PopoverPrimitive.Content.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
 | 
				
			||||||
							
								
								
									
										157
									
								
								src/components/ui/select.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										157
									
								
								src/components/ui/select.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,157 @@
 | 
				
			|||||||
 | 
					import * as React from "react"
 | 
				
			||||||
 | 
					import * as SelectPrimitive from "@radix-ui/react-select"
 | 
				
			||||||
 | 
					import { Check, ChevronDown, ChevronUp } from "lucide-react"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { cn } from "@/lib/utils"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Select = SelectPrimitive.Root
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SelectGroup = SelectPrimitive.Group
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SelectValue = SelectPrimitive.Value
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SelectTrigger = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof SelectPrimitive.Trigger>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
 | 
				
			||||||
 | 
					>(({ className, children, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <SelectPrimitive.Trigger
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn(
 | 
				
			||||||
 | 
					      "flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
 | 
				
			||||||
 | 
					      className
 | 
				
			||||||
 | 
					    )}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  >
 | 
				
			||||||
 | 
					    {children}
 | 
				
			||||||
 | 
					    <SelectPrimitive.Icon asChild>
 | 
				
			||||||
 | 
					      <ChevronDown className="h-4 w-4 opacity-50" />
 | 
				
			||||||
 | 
					    </SelectPrimitive.Icon>
 | 
				
			||||||
 | 
					  </SelectPrimitive.Trigger>
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SelectScrollUpButton = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <SelectPrimitive.ScrollUpButton
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn(
 | 
				
			||||||
 | 
					      "flex cursor-default items-center justify-center py-1",
 | 
				
			||||||
 | 
					      className
 | 
				
			||||||
 | 
					    )}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  >
 | 
				
			||||||
 | 
					    <ChevronUp className="h-4 w-4" />
 | 
				
			||||||
 | 
					  </SelectPrimitive.ScrollUpButton>
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SelectScrollDownButton = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <SelectPrimitive.ScrollDownButton
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn(
 | 
				
			||||||
 | 
					      "flex cursor-default items-center justify-center py-1",
 | 
				
			||||||
 | 
					      className
 | 
				
			||||||
 | 
					    )}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  >
 | 
				
			||||||
 | 
					    <ChevronDown className="h-4 w-4" />
 | 
				
			||||||
 | 
					  </SelectPrimitive.ScrollDownButton>
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					SelectScrollDownButton.displayName =
 | 
				
			||||||
 | 
					  SelectPrimitive.ScrollDownButton.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SelectContent = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof SelectPrimitive.Content>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
 | 
				
			||||||
 | 
					>(({ className, children, position = "popper", ...props }, ref) => (
 | 
				
			||||||
 | 
					  <SelectPrimitive.Portal>
 | 
				
			||||||
 | 
					    <SelectPrimitive.Content
 | 
				
			||||||
 | 
					      ref={ref}
 | 
				
			||||||
 | 
					      className={cn(
 | 
				
			||||||
 | 
					        "relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover text-popover-foreground shadow-md 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 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 origin-[--radix-select-content-transform-origin]",
 | 
				
			||||||
 | 
					        position === "popper" &&
 | 
				
			||||||
 | 
					          "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
 | 
				
			||||||
 | 
					        className
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					      position={position}
 | 
				
			||||||
 | 
					      {...props}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <SelectScrollUpButton />
 | 
				
			||||||
 | 
					      <SelectPrimitive.Viewport
 | 
				
			||||||
 | 
					        className={cn(
 | 
				
			||||||
 | 
					          "p-1",
 | 
				
			||||||
 | 
					          position === "popper" &&
 | 
				
			||||||
 | 
					            "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        {children}
 | 
				
			||||||
 | 
					      </SelectPrimitive.Viewport>
 | 
				
			||||||
 | 
					      <SelectScrollDownButton />
 | 
				
			||||||
 | 
					    </SelectPrimitive.Content>
 | 
				
			||||||
 | 
					  </SelectPrimitive.Portal>
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					SelectContent.displayName = SelectPrimitive.Content.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SelectLabel = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof SelectPrimitive.Label>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <SelectPrimitive.Label
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn("px-2 py-1.5 text-sm font-semibold", className)}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					SelectLabel.displayName = SelectPrimitive.Label.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SelectItem = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof SelectPrimitive.Item>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
 | 
				
			||||||
 | 
					>(({ className, children, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <SelectPrimitive.Item
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn(
 | 
				
			||||||
 | 
					      "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
 | 
				
			||||||
 | 
					      className
 | 
				
			||||||
 | 
					    )}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  >
 | 
				
			||||||
 | 
					    <span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
 | 
				
			||||||
 | 
					      <SelectPrimitive.ItemIndicator>
 | 
				
			||||||
 | 
					        <Check className="h-4 w-4" />
 | 
				
			||||||
 | 
					      </SelectPrimitive.ItemIndicator>
 | 
				
			||||||
 | 
					    </span>
 | 
				
			||||||
 | 
					    <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
 | 
				
			||||||
 | 
					  </SelectPrimitive.Item>
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					SelectItem.displayName = SelectPrimitive.Item.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SelectSeparator = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof SelectPrimitive.Separator>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <SelectPrimitive.Separator
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn("-mx-1 my-1 h-px bg-muted", className)}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					SelectSeparator.displayName = SelectPrimitive.Separator.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export {
 | 
				
			||||||
 | 
					  Select,
 | 
				
			||||||
 | 
					  SelectGroup,
 | 
				
			||||||
 | 
					  SelectValue,
 | 
				
			||||||
 | 
					  SelectTrigger,
 | 
				
			||||||
 | 
					  SelectContent,
 | 
				
			||||||
 | 
					  SelectLabel,
 | 
				
			||||||
 | 
					  SelectItem,
 | 
				
			||||||
 | 
					  SelectSeparator,
 | 
				
			||||||
 | 
					  SelectScrollUpButton,
 | 
				
			||||||
 | 
					  SelectScrollDownButton,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										29
									
								
								src/components/ui/separator.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								src/components/ui/separator.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,29 @@
 | 
				
			|||||||
 | 
					import * as React from "react"
 | 
				
			||||||
 | 
					import * as SeparatorPrimitive from "@radix-ui/react-separator"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { cn } from "@/lib/utils"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Separator = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof SeparatorPrimitive.Root>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
 | 
				
			||||||
 | 
					>(
 | 
				
			||||||
 | 
					  (
 | 
				
			||||||
 | 
					    { className, orientation = "horizontal", decorative = true, ...props },
 | 
				
			||||||
 | 
					    ref
 | 
				
			||||||
 | 
					  ) => (
 | 
				
			||||||
 | 
					    <SeparatorPrimitive.Root
 | 
				
			||||||
 | 
					      ref={ref}
 | 
				
			||||||
 | 
					      decorative={decorative}
 | 
				
			||||||
 | 
					      orientation={orientation}
 | 
				
			||||||
 | 
					      className={cn(
 | 
				
			||||||
 | 
					        "shrink-0 bg-border",
 | 
				
			||||||
 | 
					        orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
 | 
				
			||||||
 | 
					        className
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					      {...props}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					Separator.displayName = SeparatorPrimitive.Root.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export { Separator }
 | 
				
			||||||
							
								
								
									
										138
									
								
								src/components/ui/sheet.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								src/components/ui/sheet.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,138 @@
 | 
				
			|||||||
 | 
					import * as React from "react"
 | 
				
			||||||
 | 
					import * as SheetPrimitive from "@radix-ui/react-dialog"
 | 
				
			||||||
 | 
					import { cva, type VariantProps } from "class-variance-authority"
 | 
				
			||||||
 | 
					import { X } from "lucide-react"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { cn } from "@/lib/utils"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Sheet = SheetPrimitive.Root
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SheetTrigger = SheetPrimitive.Trigger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SheetClose = SheetPrimitive.Close
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SheetPortal = SheetPrimitive.Portal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SheetOverlay = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof SheetPrimitive.Overlay>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <SheetPrimitive.Overlay
 | 
				
			||||||
 | 
					    className={cn(
 | 
				
			||||||
 | 
					      "fixed inset-0 z-50 bg-black/80  data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
 | 
				
			||||||
 | 
					      className
 | 
				
			||||||
 | 
					    )}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const sheetVariants = cva(
 | 
				
			||||||
 | 
					  "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    variants: {
 | 
				
			||||||
 | 
					      side: {
 | 
				
			||||||
 | 
					        top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
 | 
				
			||||||
 | 
					        bottom:
 | 
				
			||||||
 | 
					          "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
 | 
				
			||||||
 | 
					        left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
 | 
				
			||||||
 | 
					        right:
 | 
				
			||||||
 | 
					          "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    defaultVariants: {
 | 
				
			||||||
 | 
					      side: "right",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface SheetContentProps
 | 
				
			||||||
 | 
					  extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
 | 
				
			||||||
 | 
					    VariantProps<typeof sheetVariants> {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SheetContent = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof SheetPrimitive.Content>,
 | 
				
			||||||
 | 
					  SheetContentProps
 | 
				
			||||||
 | 
					>(({ side = "right", className, children, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <SheetPortal>
 | 
				
			||||||
 | 
					    <SheetOverlay />
 | 
				
			||||||
 | 
					    <SheetPrimitive.Content
 | 
				
			||||||
 | 
					      ref={ref}
 | 
				
			||||||
 | 
					      className={cn(sheetVariants({ side }), className)}
 | 
				
			||||||
 | 
					      {...props}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
 | 
				
			||||||
 | 
					        <X className="h-4 w-4" />
 | 
				
			||||||
 | 
					        <span className="sr-only">Close</span>
 | 
				
			||||||
 | 
					      </SheetPrimitive.Close>
 | 
				
			||||||
 | 
					      {children}
 | 
				
			||||||
 | 
					    </SheetPrimitive.Content>
 | 
				
			||||||
 | 
					  </SheetPortal>
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					SheetContent.displayName = SheetPrimitive.Content.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SheetHeader = ({
 | 
				
			||||||
 | 
					  className,
 | 
				
			||||||
 | 
					  ...props
 | 
				
			||||||
 | 
					}: React.HTMLAttributes<HTMLDivElement>) => (
 | 
				
			||||||
 | 
					  <div
 | 
				
			||||||
 | 
					    className={cn(
 | 
				
			||||||
 | 
					      "flex flex-col space-y-2 text-center sm:text-left",
 | 
				
			||||||
 | 
					      className
 | 
				
			||||||
 | 
					    )}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					SheetHeader.displayName = "SheetHeader"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SheetFooter = ({
 | 
				
			||||||
 | 
					  className,
 | 
				
			||||||
 | 
					  ...props
 | 
				
			||||||
 | 
					}: React.HTMLAttributes<HTMLDivElement>) => (
 | 
				
			||||||
 | 
					  <div
 | 
				
			||||||
 | 
					    className={cn(
 | 
				
			||||||
 | 
					      "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
 | 
				
			||||||
 | 
					      className
 | 
				
			||||||
 | 
					    )}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					SheetFooter.displayName = "SheetFooter"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SheetTitle = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof SheetPrimitive.Title>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <SheetPrimitive.Title
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn("text-lg font-semibold text-foreground", className)}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					SheetTitle.displayName = SheetPrimitive.Title.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SheetDescription = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof SheetPrimitive.Description>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <SheetPrimitive.Description
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn("text-sm text-muted-foreground", className)}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					SheetDescription.displayName = SheetPrimitive.Description.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export {
 | 
				
			||||||
 | 
					  Sheet,
 | 
				
			||||||
 | 
					  SheetPortal,
 | 
				
			||||||
 | 
					  SheetOverlay,
 | 
				
			||||||
 | 
					  SheetTrigger,
 | 
				
			||||||
 | 
					  SheetClose,
 | 
				
			||||||
 | 
					  SheetContent,
 | 
				
			||||||
 | 
					  SheetHeader,
 | 
				
			||||||
 | 
					  SheetFooter,
 | 
				
			||||||
 | 
					  SheetTitle,
 | 
				
			||||||
 | 
					  SheetDescription,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										15
									
								
								src/components/ui/skeleton.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/components/ui/skeleton.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					import { cn } from "@/lib/utils"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function Skeleton({
 | 
				
			||||||
 | 
					  className,
 | 
				
			||||||
 | 
					  ...props
 | 
				
			||||||
 | 
					}: React.HTMLAttributes<HTMLDivElement>) {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div
 | 
				
			||||||
 | 
					      className={cn("animate-pulse rounded-md bg-primary/10", className)}
 | 
				
			||||||
 | 
					      {...props}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export { Skeleton }
 | 
				
			||||||
							
								
								
									
										55
									
								
								src/components/ui/tabs.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								src/components/ui/tabs.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,55 @@
 | 
				
			|||||||
 | 
					"use client"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import * as React from "react"
 | 
				
			||||||
 | 
					import * as TabsPrimitive from "@radix-ui/react-tabs"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { cn } from "@/lib/utils"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Tabs = TabsPrimitive.Root
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const TabsList = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof TabsPrimitive.List>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <TabsPrimitive.List
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn(
 | 
				
			||||||
 | 
					      "inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
 | 
				
			||||||
 | 
					      className
 | 
				
			||||||
 | 
					    )}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					TabsList.displayName = TabsPrimitive.List.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const TabsTrigger = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof TabsPrimitive.Trigger>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <TabsPrimitive.Trigger
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn(
 | 
				
			||||||
 | 
					      "inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
 | 
				
			||||||
 | 
					      className
 | 
				
			||||||
 | 
					    )}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const TabsContent = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof TabsPrimitive.Content>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <TabsPrimitive.Content
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn(
 | 
				
			||||||
 | 
					      "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
 | 
				
			||||||
 | 
					      className
 | 
				
			||||||
 | 
					    )}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					TabsContent.displayName = TabsPrimitive.Content.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export { Tabs, TabsList, TabsTrigger, TabsContent }
 | 
				
			||||||
							
								
								
									
										129
									
								
								src/components/ui/toast.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								src/components/ui/toast.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,129 @@
 | 
				
			|||||||
 | 
					"use client"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import * as React from "react"
 | 
				
			||||||
 | 
					import * as ToastPrimitives from "@radix-ui/react-toast"
 | 
				
			||||||
 | 
					import { cva, type VariantProps } from "class-variance-authority"
 | 
				
			||||||
 | 
					import { X } from "lucide-react"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { cn } from "@/lib/utils"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ToastProvider = ToastPrimitives.Provider
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ToastViewport = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof ToastPrimitives.Viewport>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <ToastPrimitives.Viewport
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn(
 | 
				
			||||||
 | 
					      "fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
 | 
				
			||||||
 | 
					      className
 | 
				
			||||||
 | 
					    )}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					ToastViewport.displayName = ToastPrimitives.Viewport.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const toastVariants = cva(
 | 
				
			||||||
 | 
					  "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    variants: {
 | 
				
			||||||
 | 
					      variant: {
 | 
				
			||||||
 | 
					        default: "border bg-background text-foreground",
 | 
				
			||||||
 | 
					        destructive:
 | 
				
			||||||
 | 
					          "destructive group border-destructive bg-destructive text-destructive-foreground",
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    defaultVariants: {
 | 
				
			||||||
 | 
					      variant: "default",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Toast = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof ToastPrimitives.Root>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
 | 
				
			||||||
 | 
					    VariantProps<typeof toastVariants>
 | 
				
			||||||
 | 
					>(({ className, variant, ...props }, ref) => {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <ToastPrimitives.Root
 | 
				
			||||||
 | 
					      ref={ref}
 | 
				
			||||||
 | 
					      className={cn(toastVariants({ variant }), className)}
 | 
				
			||||||
 | 
					      {...props}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					Toast.displayName = ToastPrimitives.Root.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ToastAction = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof ToastPrimitives.Action>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <ToastPrimitives.Action
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn(
 | 
				
			||||||
 | 
					      "inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
 | 
				
			||||||
 | 
					      className
 | 
				
			||||||
 | 
					    )}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					ToastAction.displayName = ToastPrimitives.Action.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ToastClose = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof ToastPrimitives.Close>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <ToastPrimitives.Close
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn(
 | 
				
			||||||
 | 
					      "absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
 | 
				
			||||||
 | 
					      className
 | 
				
			||||||
 | 
					    )}
 | 
				
			||||||
 | 
					    toast-close=""
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  >
 | 
				
			||||||
 | 
					    <X className="h-4 w-4" />
 | 
				
			||||||
 | 
					  </ToastPrimitives.Close>
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					ToastClose.displayName = ToastPrimitives.Close.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ToastTitle = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof ToastPrimitives.Title>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <ToastPrimitives.Title
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn("text-sm font-semibold [&+div]:text-xs", className)}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					ToastTitle.displayName = ToastPrimitives.Title.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ToastDescription = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof ToastPrimitives.Description>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <ToastPrimitives.Description
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn("text-sm opacity-90", className)}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					ToastDescription.displayName = ToastPrimitives.Description.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ToastActionElement = React.ReactElement<typeof ToastAction>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export {
 | 
				
			||||||
 | 
					  type ToastProps,
 | 
				
			||||||
 | 
					  type ToastActionElement,
 | 
				
			||||||
 | 
					  ToastProvider,
 | 
				
			||||||
 | 
					  ToastViewport,
 | 
				
			||||||
 | 
					  Toast,
 | 
				
			||||||
 | 
					  ToastTitle,
 | 
				
			||||||
 | 
					  ToastDescription,
 | 
				
			||||||
 | 
					  ToastClose,
 | 
				
			||||||
 | 
					  ToastAction,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										35
									
								
								src/components/ui/toaster.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/components/ui/toaster.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,35 @@
 | 
				
			|||||||
 | 
					"use client"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { useToast } from "@/hooks/use-toast"
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  Toast,
 | 
				
			||||||
 | 
					  ToastClose,
 | 
				
			||||||
 | 
					  ToastDescription,
 | 
				
			||||||
 | 
					  ToastProvider,
 | 
				
			||||||
 | 
					  ToastTitle,
 | 
				
			||||||
 | 
					  ToastViewport,
 | 
				
			||||||
 | 
					} from "@/components/ui/toast"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function Toaster() {
 | 
				
			||||||
 | 
					  const { toasts } = useToast()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <ToastProvider>
 | 
				
			||||||
 | 
					      {toasts.map(function ({ id, title, description, action, ...props }) {
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					          <Toast key={id} {...props}>
 | 
				
			||||||
 | 
					            <div className="grid gap-1">
 | 
				
			||||||
 | 
					              {title && <ToastTitle>{title}</ToastTitle>}
 | 
				
			||||||
 | 
					              {description && (
 | 
				
			||||||
 | 
					                <ToastDescription>{description}</ToastDescription>
 | 
				
			||||||
 | 
					              )}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            {action}
 | 
				
			||||||
 | 
					            <ToastClose />
 | 
				
			||||||
 | 
					          </Toast>
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					      })}
 | 
				
			||||||
 | 
					      <ToastViewport />
 | 
				
			||||||
 | 
					    </ToastProvider>
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										191
									
								
								src/hooks/use-toast.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										191
									
								
								src/hooks/use-toast.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,191 @@
 | 
				
			|||||||
 | 
					import * as React from "react"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import type {
 | 
				
			||||||
 | 
					  ToastActionElement,
 | 
				
			||||||
 | 
					  ToastProps,
 | 
				
			||||||
 | 
					} from "@/components/ui/toast"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const TOAST_LIMIT = 1
 | 
				
			||||||
 | 
					const TOAST_REMOVE_DELAY = 1000000
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ToasterToast = ToastProps & {
 | 
				
			||||||
 | 
					  id: string
 | 
				
			||||||
 | 
					  title?: React.ReactNode
 | 
				
			||||||
 | 
					  description?: React.ReactNode
 | 
				
			||||||
 | 
					  action?: ToastActionElement
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const actionTypes = {
 | 
				
			||||||
 | 
					  ADD_TOAST: "ADD_TOAST",
 | 
				
			||||||
 | 
					  UPDATE_TOAST: "UPDATE_TOAST",
 | 
				
			||||||
 | 
					  DISMISS_TOAST: "DISMISS_TOAST",
 | 
				
			||||||
 | 
					  REMOVE_TOAST: "REMOVE_TOAST",
 | 
				
			||||||
 | 
					} as const
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let count = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function genId() {
 | 
				
			||||||
 | 
					  count = (count + 1) % Number.MAX_SAFE_INTEGER
 | 
				
			||||||
 | 
					  return count.toString()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ActionType = typeof actionTypes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Action =
 | 
				
			||||||
 | 
					  | {
 | 
				
			||||||
 | 
					      type: ActionType["ADD_TOAST"]
 | 
				
			||||||
 | 
					      toast: ToasterToast
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  | {
 | 
				
			||||||
 | 
					      type: ActionType["UPDATE_TOAST"]
 | 
				
			||||||
 | 
					      toast: Partial<ToasterToast>
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  | {
 | 
				
			||||||
 | 
					      type: ActionType["DISMISS_TOAST"]
 | 
				
			||||||
 | 
					      toastId?: ToasterToast["id"]
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  | {
 | 
				
			||||||
 | 
					      type: ActionType["REMOVE_TOAST"]
 | 
				
			||||||
 | 
					      toastId?: ToasterToast["id"]
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface State {
 | 
				
			||||||
 | 
					  toasts: ToasterToast[]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const addToRemoveQueue = (toastId: string) => {
 | 
				
			||||||
 | 
					  if (toastTimeouts.has(toastId)) {
 | 
				
			||||||
 | 
					    return
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const timeout = setTimeout(() => {
 | 
				
			||||||
 | 
					    toastTimeouts.delete(toastId)
 | 
				
			||||||
 | 
					    dispatch({
 | 
				
			||||||
 | 
					      type: "REMOVE_TOAST",
 | 
				
			||||||
 | 
					      toastId: toastId,
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  }, TOAST_REMOVE_DELAY)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  toastTimeouts.set(toastId, timeout)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const reducer = (state: State, action: Action): State => {
 | 
				
			||||||
 | 
					  switch (action.type) {
 | 
				
			||||||
 | 
					    case "ADD_TOAST":
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        ...state,
 | 
				
			||||||
 | 
					        toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    case "UPDATE_TOAST":
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        ...state,
 | 
				
			||||||
 | 
					        toasts: state.toasts.map((t) =>
 | 
				
			||||||
 | 
					          t.id === action.toast.id ? { ...t, ...action.toast } : t
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    case "DISMISS_TOAST": {
 | 
				
			||||||
 | 
					      const { toastId } = action
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // ! Side effects ! - This could be extracted into a dismissToast() action,
 | 
				
			||||||
 | 
					      // but I'll keep it here for simplicity
 | 
				
			||||||
 | 
					      if (toastId) {
 | 
				
			||||||
 | 
					        addToRemoveQueue(toastId)
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        state.toasts.forEach((toast) => {
 | 
				
			||||||
 | 
					          addToRemoveQueue(toast.id)
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        ...state,
 | 
				
			||||||
 | 
					        toasts: state.toasts.map((t) =>
 | 
				
			||||||
 | 
					          t.id === toastId || toastId === undefined
 | 
				
			||||||
 | 
					            ? {
 | 
				
			||||||
 | 
					                ...t,
 | 
				
			||||||
 | 
					                open: false,
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            : t
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    case "REMOVE_TOAST":
 | 
				
			||||||
 | 
					      if (action.toastId === undefined) {
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					          ...state,
 | 
				
			||||||
 | 
					          toasts: [],
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        ...state,
 | 
				
			||||||
 | 
					        toasts: state.toasts.filter((t) => t.id !== action.toastId),
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const listeners: ((state: State) => void)[] = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let memoryState: State = { toasts: [] }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function dispatch(action: Action) {
 | 
				
			||||||
 | 
					  memoryState = reducer(memoryState, action)
 | 
				
			||||||
 | 
					  listeners.forEach((listener) => {
 | 
				
			||||||
 | 
					    listener(memoryState)
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Toast = Omit<ToasterToast, "id">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function toast({ ...props }: Toast) {
 | 
				
			||||||
 | 
					  const id = genId()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const update = (props: ToasterToast) =>
 | 
				
			||||||
 | 
					    dispatch({
 | 
				
			||||||
 | 
					      type: "UPDATE_TOAST",
 | 
				
			||||||
 | 
					      toast: { ...props, id },
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  dispatch({
 | 
				
			||||||
 | 
					    type: "ADD_TOAST",
 | 
				
			||||||
 | 
					    toast: {
 | 
				
			||||||
 | 
					      ...props,
 | 
				
			||||||
 | 
					      id,
 | 
				
			||||||
 | 
					      open: true,
 | 
				
			||||||
 | 
					      onOpenChange: (open) => {
 | 
				
			||||||
 | 
					        if (!open) dismiss()
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    id: id,
 | 
				
			||||||
 | 
					    dismiss,
 | 
				
			||||||
 | 
					    update,
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function useToast() {
 | 
				
			||||||
 | 
					  const [state, setState] = React.useState<State>(memoryState)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  React.useEffect(() => {
 | 
				
			||||||
 | 
					    listeners.push(setState)
 | 
				
			||||||
 | 
					    return () => {
 | 
				
			||||||
 | 
					      const index = listeners.indexOf(setState)
 | 
				
			||||||
 | 
					      if (index > -1) {
 | 
				
			||||||
 | 
					        listeners.splice(index, 1)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [state])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    ...state,
 | 
				
			||||||
 | 
					    toast,
 | 
				
			||||||
 | 
					    dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export { useToast, toast }
 | 
				
			||||||
							
								
								
									
										73
									
								
								src/index.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								src/index.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,73 @@
 | 
				
			|||||||
 | 
					@tailwind base;
 | 
				
			||||||
 | 
					@tailwind components;
 | 
				
			||||||
 | 
					@tailwind utilities;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					body {
 | 
				
			||||||
 | 
					  font-family: Arial, Helvetica, sans-serif;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@layer base {
 | 
				
			||||||
 | 
					  :root {
 | 
				
			||||||
 | 
					    --background: 0 0% 100%;
 | 
				
			||||||
 | 
					    --foreground: 222.2 84% 4.9%;
 | 
				
			||||||
 | 
					    --card: 0 0% 100%;
 | 
				
			||||||
 | 
					    --card-foreground: 222.2 84% 4.9%;
 | 
				
			||||||
 | 
					    --popover: 0 0% 100%;
 | 
				
			||||||
 | 
					    --popover-foreground: 222.2 84% 4.9%;
 | 
				
			||||||
 | 
					    --primary: 221.2 83.2% 53.3%;
 | 
				
			||||||
 | 
					    --primary-foreground: 210 40% 98%;
 | 
				
			||||||
 | 
					    --secondary: 210 40% 96.1%;
 | 
				
			||||||
 | 
					    --secondary-foreground: 222.2 47.4% 11.2%;
 | 
				
			||||||
 | 
					    --muted: 210 40% 96.1%;
 | 
				
			||||||
 | 
					    --muted-foreground: 215.4 16.3% 46.9%;
 | 
				
			||||||
 | 
					    --accent: 210 40% 96.1%;
 | 
				
			||||||
 | 
					    --accent-foreground: 222.2 47.4% 11.2%;
 | 
				
			||||||
 | 
					    --destructive: 0 84.2% 60.2%;
 | 
				
			||||||
 | 
					    --destructive-foreground: 210 40% 98%;
 | 
				
			||||||
 | 
					    --border: 214.3 31.8% 91.4%;
 | 
				
			||||||
 | 
					    --input: 214.3 31.8% 91.4%;
 | 
				
			||||||
 | 
					    --ring: 221.2 83.2% 53.3%;
 | 
				
			||||||
 | 
					    --radius: 0.5rem;
 | 
				
			||||||
 | 
					    --chart-1: 12 76% 61%;
 | 
				
			||||||
 | 
					    --chart-2: 173 58% 39%;
 | 
				
			||||||
 | 
					    --chart-3: 197 37% 24%;
 | 
				
			||||||
 | 
					    --chart-4: 43 74% 66%;
 | 
				
			||||||
 | 
					    --chart-5: 27 87% 67%;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .dark {
 | 
				
			||||||
 | 
					    --background: 222.2 84% 4.9%;
 | 
				
			||||||
 | 
					    --foreground: 210 40% 98%;
 | 
				
			||||||
 | 
					    --card: 222.2 84% 4.9%;
 | 
				
			||||||
 | 
					    --card-foreground: 210 40% 98%;
 | 
				
			||||||
 | 
					    --popover: 222.2 84% 4.9%;
 | 
				
			||||||
 | 
					    --popover-foreground: 210 40% 98%;
 | 
				
			||||||
 | 
					    --primary: 217.2 91.2% 59.8%;
 | 
				
			||||||
 | 
					    --primary-foreground: 222.2 47.4% 11.2%;
 | 
				
			||||||
 | 
					    --secondary: 217.2 32.6% 17.5%;
 | 
				
			||||||
 | 
					    --secondary-foreground: 210 40% 98%;
 | 
				
			||||||
 | 
					    --muted: 217.2 32.6% 17.5%;
 | 
				
			||||||
 | 
					    --muted-foreground: 215 20.2% 65.1%;
 | 
				
			||||||
 | 
					    --accent: 217.2 32.6% 17.5%;
 | 
				
			||||||
 | 
					    --accent-foreground: 210 40% 98%;
 | 
				
			||||||
 | 
					    --destructive: 0 62.8% 30.6%;
 | 
				
			||||||
 | 
					    --destructive-foreground: 210 40% 98%;
 | 
				
			||||||
 | 
					    --border: 217.2 32.6% 17.5%;
 | 
				
			||||||
 | 
					    --input: 217.2 32.6% 17.5%;
 | 
				
			||||||
 | 
					    --ring: 224.3 76.3% 48%;
 | 
				
			||||||
 | 
					    --chart-1: 220 70% 50%;
 | 
				
			||||||
 | 
					    --chart-2: 160 60% 45%;
 | 
				
			||||||
 | 
					    --chart-3: 30 80% 55%;
 | 
				
			||||||
 | 
					    --chart-4: 280 65% 60%;
 | 
				
			||||||
 | 
					    --chart-5: 340 75% 55%;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@layer base {
 | 
				
			||||||
 | 
					  * {
 | 
				
			||||||
 | 
					    @apply border-border;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  body {
 | 
				
			||||||
 | 
					    @apply bg-background text-foreground;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										176
									
								
								src/lib/api.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										176
									
								
								src/lib/api.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,176 @@
 | 
				
			|||||||
 | 
					import axios from 'axios';
 | 
				
			||||||
 | 
					import { toast } from 'sonner';
 | 
				
			||||||
 | 
					import { getToken, clearToken } from './auth';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Set up API base URLs
 | 
				
			||||||
 | 
					const AUTH_API_URL = 'https://io-a.monasefloadbalancer.ir/auth';
 | 
				
			||||||
 | 
					const DATA_API_URL = 'https://io-e-cf.monasefloadbalancer.ir/api/user';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Create axios instances
 | 
				
			||||||
 | 
					const authApi = axios.create({
 | 
				
			||||||
 | 
					  baseURL: AUTH_API_URL,
 | 
				
			||||||
 | 
					  headers: {
 | 
				
			||||||
 | 
					    'Content-Type': 'application/json',
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const dataApi = axios.create({
 | 
				
			||||||
 | 
					  baseURL: DATA_API_URL,
 | 
				
			||||||
 | 
					  headers: {
 | 
				
			||||||
 | 
					    'Content-Type': 'application/json',
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Add authorization interceptor for data API
 | 
				
			||||||
 | 
					dataApi.interceptors.request.use(
 | 
				
			||||||
 | 
					  (config) => {
 | 
				
			||||||
 | 
					    const token = getToken();
 | 
				
			||||||
 | 
					    if (token) {
 | 
				
			||||||
 | 
					      config.headers.Authorization = `Bearer ${token}`;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return config;
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  (error) => Promise.reject(error)
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Add response interceptor to handle unauthorized errors
 | 
				
			||||||
 | 
					const handleUnauthorized = (error: any) => {
 | 
				
			||||||
 | 
					  if (error.response && error.response.status === 401) {
 | 
				
			||||||
 | 
					    toast.error('Your session has expired. Please login again.');
 | 
				
			||||||
 | 
					    clearToken();
 | 
				
			||||||
 | 
					    // Redirect to login page
 | 
				
			||||||
 | 
					    window.location.href = '/login';
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return Promise.reject(error);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					dataApi.interceptors.response.use((response) => response, handleUnauthorized);
 | 
				
			||||||
 | 
					authApi.interceptors.response.use((response) => response, handleUnauthorized);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Auth API functions
 | 
				
			||||||
 | 
					export const registerUser = async (username: string, password: string, email: string) => {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const response = await authApi.post('/register', { username, password, email });
 | 
				
			||||||
 | 
					    return response.data;
 | 
				
			||||||
 | 
					  } catch (error: any) {
 | 
				
			||||||
 | 
					    throw error.response?.data || { success: false, message: 'Network error' };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const loginUser = async (username: string, password: string) => {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const response = await authApi.post('/login', { username, password });
 | 
				
			||||||
 | 
					    return response.data;
 | 
				
			||||||
 | 
					  } catch (error: any) {
 | 
				
			||||||
 | 
					    throw error.response?.data || { success: false, message: 'Network error' };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Data API functions
 | 
				
			||||||
 | 
					export const getAllFeeds = async () => {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const response = await dataApi.get('/GetAllFeeds');
 | 
				
			||||||
 | 
					    return response.data;
 | 
				
			||||||
 | 
					  } catch (error: any) {
 | 
				
			||||||
 | 
					    throw error.response?.data || { success: false, message: 'Network error' };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const addNewFeed = async () => {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const response = await dataApi.get('/AddNewFeed');
 | 
				
			||||||
 | 
					    return response.data;
 | 
				
			||||||
 | 
					  } catch (error: any) {
 | 
				
			||||||
 | 
					    throw error.response?.data || { success: false, message: 'Network error' };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const deleteFeed = async (feedId: string) => {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const response = await dataApi.delete('/DeleteFeed', {
 | 
				
			||||||
 | 
					      data: { feedId }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    return response.data;
 | 
				
			||||||
 | 
					  } catch (error: any) {
 | 
				
			||||||
 | 
					    throw error.response?.data || { success: false, message: 'Network error' };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getFeedDataTimeRange = async (
 | 
				
			||||||
 | 
					  feedId: string,
 | 
				
			||||||
 | 
					  startTime: string,
 | 
				
			||||||
 | 
					  endTime: string
 | 
				
			||||||
 | 
					) => {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const response = await dataApi.get(`/GetFeedDataTimeRange/${feedId}/${startTime}/${endTime}`);
 | 
				
			||||||
 | 
					    return response.data;
 | 
				
			||||||
 | 
					  } catch (error: any) {
 | 
				
			||||||
 | 
					    throw error.response?.data || { success: false, message: 'Network error' };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// API Key functions
 | 
				
			||||||
 | 
					export const createApiKey = async () => {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const token = getToken();
 | 
				
			||||||
 | 
					    if (!token) {
 | 
				
			||||||
 | 
					      throw new Error('Authentication required');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const response = await authApi.get('/createapikey', {
 | 
				
			||||||
 | 
					      headers: {
 | 
				
			||||||
 | 
					        Authorization: `Bearer ${token}`
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    return response.data;
 | 
				
			||||||
 | 
					  } catch (error: any) {
 | 
				
			||||||
 | 
					    throw error.response?.data || { success: false, message: 'Network error' };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getUserApiKeys = async () => {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const token = getToken();
 | 
				
			||||||
 | 
					    if (!token) {
 | 
				
			||||||
 | 
					      throw new Error('Authentication required');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const response = await authApi.get('/getuserapikeys', {
 | 
				
			||||||
 | 
					      headers: {
 | 
				
			||||||
 | 
					        Authorization: `Bearer ${token}`
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    return response.data;
 | 
				
			||||||
 | 
					  } catch (error: any) {
 | 
				
			||||||
 | 
					    throw error.response?.data || { success: false, message: 'Network error' };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const deleteApiKey = async (ApiKey: string) => {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const token = getToken();
 | 
				
			||||||
 | 
					    if (!token) {
 | 
				
			||||||
 | 
					      throw new Error('Authentication required');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const response = await authApi.delete('/deleteapikey', {
 | 
				
			||||||
 | 
					      headers: {
 | 
				
			||||||
 | 
					        Authorization: `Bearer ${token}`
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      data: { ApiKey }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    return response.data;
 | 
				
			||||||
 | 
					  } catch (error: any) {
 | 
				
			||||||
 | 
					    throw error.response?.data || { success: false, message: 'Network error' };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const verifyEmail = async (token: string) => {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    // Call the verify email endpoint with no extra headers or data
 | 
				
			||||||
 | 
					    const response = await authApi.get(`/verifyemail/${token}`);
 | 
				
			||||||
 | 
					    return response.data;
 | 
				
			||||||
 | 
					  } catch (error: any) {
 | 
				
			||||||
 | 
					    throw error.response?.data || { success: false, message: 'Network error' };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										63
									
								
								src/lib/auth.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								src/lib/auth.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,63 @@
 | 
				
			|||||||
 | 
					import { jwtDecode } from 'jwt-decode';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const TOKEN_KEY = 'auth_token';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface JwtPayload {
 | 
				
			||||||
 | 
					  exp: number;
 | 
				
			||||||
 | 
					  [key: string]: any;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Authentication utilities
 | 
				
			||||||
 | 
					export const saveToken = (token: string): void => {
 | 
				
			||||||
 | 
					  localStorage.setItem(TOKEN_KEY, token);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const setToken = (token: string): void => {
 | 
				
			||||||
 | 
					  localStorage.setItem(TOKEN_KEY, token);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getToken = (): string | null => {
 | 
				
			||||||
 | 
					  return localStorage.getItem(TOKEN_KEY);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const clearToken = (): void => {
 | 
				
			||||||
 | 
					  localStorage.removeItem(TOKEN_KEY);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const isAuthenticated = (): boolean => {
 | 
				
			||||||
 | 
					  const token = getToken();
 | 
				
			||||||
 | 
					  if (!token) return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const decoded = jwtDecode<JwtPayload>(token);
 | 
				
			||||||
 | 
					    // Check if token is expired
 | 
				
			||||||
 | 
					    const currentTime = Date.now() / 1000;
 | 
				
			||||||
 | 
					    return decoded.exp > currentTime;
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    clearToken(); // Clear invalid token
 | 
				
			||||||
 | 
					    return false;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Handle authentication errors
 | 
				
			||||||
 | 
					export const handleAuthError = (error: any) => {
 | 
				
			||||||
 | 
					  if (error.response && error.response.status === 401) {
 | 
				
			||||||
 | 
					    clearToken();
 | 
				
			||||||
 | 
					    window.location.href = '/login';
 | 
				
			||||||
 | 
					    return { success: false, message: 'Authentication failed. Please login again.' };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return error;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getUsername = (): string | null => {
 | 
				
			||||||
 | 
					  const token = getToken();
 | 
				
			||||||
 | 
					  if (!token) return null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const decoded = jwtDecode<JwtPayload>(token);
 | 
				
			||||||
 | 
					    // Assuming the username claim is stored in the 'name' field
 | 
				
			||||||
 | 
					    return decoded['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name'] || null;
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										138
									
								
								src/lib/store.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								src/lib/store.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,138 @@
 | 
				
			|||||||
 | 
					import { create } from 'zustand';
 | 
				
			||||||
 | 
					import { Feed, FeedDataPoint } from './types';
 | 
				
			||||||
 | 
					import { getAllFeeds, getFeedDataTimeRange, deleteFeed } from './api';
 | 
				
			||||||
 | 
					import { toast } from 'sonner';
 | 
				
			||||||
 | 
					import { getUTCDate } from './timeRanges';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface FeedState {
 | 
				
			||||||
 | 
					  feeds: Feed[];
 | 
				
			||||||
 | 
					  selectedFeed: Feed | null;
 | 
				
			||||||
 | 
					  feedData: FeedDataPoint[];
 | 
				
			||||||
 | 
					  isLoading: boolean;
 | 
				
			||||||
 | 
					  error: string | null;
 | 
				
			||||||
 | 
					  timeRange: {
 | 
				
			||||||
 | 
					    startTime: string;
 | 
				
			||||||
 | 
					    endTime: string;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Actions
 | 
				
			||||||
 | 
					  fetchFeeds: () => Promise<void>;
 | 
				
			||||||
 | 
					  selectFeed: (feed: Feed) => void;
 | 
				
			||||||
 | 
					  fetchFeedData: (feedId: string, startTime: string, endTime: string) => Promise<void>;
 | 
				
			||||||
 | 
					  refreshCurrentFeedData: () => Promise<void>;
 | 
				
			||||||
 | 
					  setTimeRange: (startTime: string, endTime: string) => void;
 | 
				
			||||||
 | 
					  deleteFeed: (feedId: string) => Promise<void>;
 | 
				
			||||||
 | 
					  clearError: () => void;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const useFeedStore = create<FeedState>((set, get) => ({
 | 
				
			||||||
 | 
					  feeds: [],
 | 
				
			||||||
 | 
					  selectedFeed: null,
 | 
				
			||||||
 | 
					  feedData: [],
 | 
				
			||||||
 | 
					  isLoading: false,
 | 
				
			||||||
 | 
					  error: null,
 | 
				
			||||||
 | 
					  timeRange: {
 | 
				
			||||||
 | 
					    startTime: new Date(Date.now() - 30 * 60 * 1000).toISOString(), // 30 minutes ago
 | 
				
			||||||
 | 
					    endTime: new Date().toISOString(),
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  fetchFeeds: async () => {
 | 
				
			||||||
 | 
					    set({ isLoading: true, error: null });
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const feeds = await getAllFeeds();
 | 
				
			||||||
 | 
					      set({ feeds, isLoading: false });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Select the first feed if there is one and none is currently selected
 | 
				
			||||||
 | 
					      if (feeds.length > 0 && !get().selectedFeed) {
 | 
				
			||||||
 | 
					        set({ selectedFeed: feeds[0] });
 | 
				
			||||||
 | 
					        const { timeRange } = get();
 | 
				
			||||||
 | 
					        get().fetchFeedData(feeds[0].id, timeRange.startTime, timeRange.endTime);
 | 
				
			||||||
 | 
					      } else if (get().selectedFeed) {
 | 
				
			||||||
 | 
					        // If we have a selected feed, check if it still exists in the updated feeds list
 | 
				
			||||||
 | 
					        const currentSelectedFeed = get().selectedFeed;
 | 
				
			||||||
 | 
					        if (currentSelectedFeed) {
 | 
				
			||||||
 | 
					          const selectedFeedId = currentSelectedFeed.id;
 | 
				
			||||||
 | 
					          const feedStillExists = feeds.some((feed: Feed) => feed.id === selectedFeedId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (!feedStillExists && feeds.length > 0) {
 | 
				
			||||||
 | 
					            // If the selected feed no longer exists, select the first feed
 | 
				
			||||||
 | 
					            set({ selectedFeed: feeds[0] });
 | 
				
			||||||
 | 
					            const { timeRange } = get();
 | 
				
			||||||
 | 
					            get().fetchFeedData(feeds[0].id, timeRange.startTime, timeRange.endTime);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      set({
 | 
				
			||||||
 | 
					        isLoading: false,
 | 
				
			||||||
 | 
					        error: error instanceof Error ? error.message : 'Failed to fetch feeds'
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      toast.error('Failed to fetch feeds');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  selectFeed: (feed) => {
 | 
				
			||||||
 | 
					    set({ selectedFeed: feed });
 | 
				
			||||||
 | 
					    const { timeRange } = get();
 | 
				
			||||||
 | 
					    get().fetchFeedData(feed.id, timeRange.startTime, timeRange.endTime);
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  fetchFeedData: async (feedId, startTime, endTime) => {
 | 
				
			||||||
 | 
					    set({ isLoading: true, error: null });
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const data = await getFeedDataTimeRange(feedId, startTime, endTime);
 | 
				
			||||||
 | 
					      set({ feedData: data, isLoading: false });
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      set({
 | 
				
			||||||
 | 
					        isLoading: false,
 | 
				
			||||||
 | 
					        error: error instanceof Error ? error.message : 'Failed to fetch feed data'
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      toast.error('Failed to fetch feed data');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  refreshCurrentFeedData: async () => {
 | 
				
			||||||
 | 
					    const { selectedFeed, timeRange } = get();
 | 
				
			||||||
 | 
					    if (selectedFeed) {
 | 
				
			||||||
 | 
					      // Get the selected time range option value based on current timeRange duration
 | 
				
			||||||
 | 
					      const currentStartTime = getUTCDate(new Date(timeRange.startTime));
 | 
				
			||||||
 | 
					      const currentEndTime = getUTCDate(new Date(timeRange.endTime));
 | 
				
			||||||
 | 
					      const durationMs = currentEndTime.getTime() - currentStartTime.getTime();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Create new time range with the same duration but updated to the current time
 | 
				
			||||||
 | 
					      const newEndTime = getUTCDate(new Date());
 | 
				
			||||||
 | 
					      const newStartTime = getUTCDate(new Date(newEndTime.getTime() - durationMs));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Update the time range in the store
 | 
				
			||||||
 | 
					      set({
 | 
				
			||||||
 | 
					        timeRange: {
 | 
				
			||||||
 | 
					          startTime: newStartTime.toISOString(),
 | 
				
			||||||
 | 
					          endTime: newEndTime.toISOString()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Fetch data with the updated time range
 | 
				
			||||||
 | 
					      await get().fetchFeedData(
 | 
				
			||||||
 | 
					        selectedFeed.id,
 | 
				
			||||||
 | 
					        newStartTime.toISOString(),
 | 
				
			||||||
 | 
					        newEndTime.toISOString()
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  setTimeRange: (startTime, endTime) => {
 | 
				
			||||||
 | 
					    set({ timeRange: { startTime, endTime } });
 | 
				
			||||||
 | 
					    const { selectedFeed } = get();
 | 
				
			||||||
 | 
					    if (selectedFeed) {
 | 
				
			||||||
 | 
					      get().fetchFeedData(selectedFeed.id, startTime, endTime);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  deleteFeed: async (feedId) => {
 | 
				
			||||||
 | 
					    await deleteFeed(feedId);
 | 
				
			||||||
 | 
					    // Update the feeds list
 | 
				
			||||||
 | 
					    await get().fetchFeeds();
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  clearError: () => set({ error: null }),
 | 
				
			||||||
 | 
					}));
 | 
				
			||||||
							
								
								
									
										72
									
								
								src/lib/theme-provider.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								src/lib/theme-provider.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,72 @@
 | 
				
			|||||||
 | 
					import { createContext, useContext, useEffect, useState } from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Theme = "dark" | "light" | "system";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface ThemeProviderProps {
 | 
				
			||||||
 | 
					  children: React.ReactNode;
 | 
				
			||||||
 | 
					  defaultTheme?: Theme;
 | 
				
			||||||
 | 
					  storageKey?: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface ThemeProviderState {
 | 
				
			||||||
 | 
					  theme: Theme;
 | 
				
			||||||
 | 
					  setTheme: (theme: Theme) => void;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const initialState: ThemeProviderState = {
 | 
				
			||||||
 | 
					  theme: "system",
 | 
				
			||||||
 | 
					  setTheme: () => null,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function ThemeProvider({
 | 
				
			||||||
 | 
					  children,
 | 
				
			||||||
 | 
					  defaultTheme = "system",
 | 
				
			||||||
 | 
					  storageKey = "iot-plotter-ui-theme",
 | 
				
			||||||
 | 
					  ...props
 | 
				
			||||||
 | 
					}: ThemeProviderProps) {
 | 
				
			||||||
 | 
					  const [theme, setTheme] = useState<Theme>(
 | 
				
			||||||
 | 
					    () => (localStorage.getItem(storageKey) as Theme) || defaultTheme
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    const root = window.document.documentElement;
 | 
				
			||||||
 | 
					    root.classList.remove("light", "dark");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (theme === "system") {
 | 
				
			||||||
 | 
					      const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
 | 
				
			||||||
 | 
					        .matches
 | 
				
			||||||
 | 
					        ? "dark"
 | 
				
			||||||
 | 
					        : "light";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      root.classList.add(systemTheme);
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    root.classList.add(theme);
 | 
				
			||||||
 | 
					  }, [theme]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const value = {
 | 
				
			||||||
 | 
					    theme,
 | 
				
			||||||
 | 
					    setTheme: (theme: Theme) => {
 | 
				
			||||||
 | 
					      localStorage.setItem(storageKey, theme);
 | 
				
			||||||
 | 
					      setTheme(theme);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <ThemeProviderContext.Provider {...props} value={value}>
 | 
				
			||||||
 | 
					      {children}
 | 
				
			||||||
 | 
					    </ThemeProviderContext.Provider>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const useTheme = () => {
 | 
				
			||||||
 | 
					  const context = useContext(ThemeProviderContext);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (context === undefined)
 | 
				
			||||||
 | 
					    throw new Error("useTheme must be used within a ThemeProvider");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return context;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										152
									
								
								src/lib/timeRanges.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										152
									
								
								src/lib/timeRanges.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,152 @@
 | 
				
			|||||||
 | 
					import { TimeRangeOption } from './types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Helper function to get ISO string from Date
 | 
				
			||||||
 | 
					const getISOString = (date: Date): string => date.toISOString();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Get current time
 | 
				
			||||||
 | 
					const getNow = (): Date => new Date();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Function to ensure all dates are in UTC
 | 
				
			||||||
 | 
					export const getUTCDate = (date: Date): Date => {
 | 
				
			||||||
 | 
					  return new Date(Date.UTC(
 | 
				
			||||||
 | 
					    date.getUTCFullYear(),
 | 
				
			||||||
 | 
					    date.getUTCMonth(),
 | 
				
			||||||
 | 
					    date.getUTCDate(),
 | 
				
			||||||
 | 
					    date.getUTCHours(),
 | 
				
			||||||
 | 
					    date.getUTCMinutes(),
 | 
				
			||||||
 | 
					    date.getUTCSeconds()
 | 
				
			||||||
 | 
					  ));
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Get time range options for the dropdown
 | 
				
			||||||
 | 
					export const getTimeRangeOptions = (): TimeRangeOption[] => {
 | 
				
			||||||
 | 
					  return [
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      label: '30 Minutes',
 | 
				
			||||||
 | 
					      value: '30min',
 | 
				
			||||||
 | 
					      getTimeRange: () => {
 | 
				
			||||||
 | 
					        const endTime = getNow();
 | 
				
			||||||
 | 
					        const startTime = new Date(endTime.getTime() - 30 * 60 * 1000);
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					          startTime: getISOString(getUTCDate(startTime)),
 | 
				
			||||||
 | 
					          endTime: getISOString(getUTCDate(endTime)),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      label: '1 Hour',
 | 
				
			||||||
 | 
					      value: '1hour',
 | 
				
			||||||
 | 
					      getTimeRange: () => {
 | 
				
			||||||
 | 
					        const endTime = getNow();
 | 
				
			||||||
 | 
					        const startTime = new Date(endTime.getTime() - 60 * 60 * 1000);
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					          startTime: getISOString(getUTCDate(startTime)),
 | 
				
			||||||
 | 
					          endTime: getISOString(getUTCDate(endTime)),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      label: '3 Hours',
 | 
				
			||||||
 | 
					      value: '3hours',
 | 
				
			||||||
 | 
					      getTimeRange: () => {
 | 
				
			||||||
 | 
					        const endTime = getNow();
 | 
				
			||||||
 | 
					        const startTime = new Date(endTime.getTime() - 3 * 60 * 60 * 1000);
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					          startTime: getISOString(getUTCDate(startTime)),
 | 
				
			||||||
 | 
					          endTime: getISOString(getUTCDate(endTime)),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      label: '12 Hours',
 | 
				
			||||||
 | 
					      value: '12hours',
 | 
				
			||||||
 | 
					      getTimeRange: () => {
 | 
				
			||||||
 | 
					        const endTime = getNow();
 | 
				
			||||||
 | 
					        const startTime = new Date(endTime.getTime() - 12 * 60 * 60 * 1000);
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					          startTime: getISOString(getUTCDate(startTime)),
 | 
				
			||||||
 | 
					          endTime: getISOString(getUTCDate(endTime)),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      label: '24 Hours',
 | 
				
			||||||
 | 
					      value: '24hours',
 | 
				
			||||||
 | 
					      getTimeRange: () => {
 | 
				
			||||||
 | 
					        const endTime = getNow();
 | 
				
			||||||
 | 
					        const startTime = new Date(endTime.getTime() - 24 * 60 * 60 * 1000);
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					          startTime: getISOString(getUTCDate(startTime)),
 | 
				
			||||||
 | 
					          endTime: getISOString(getUTCDate(endTime)),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      label: '48 Hours',
 | 
				
			||||||
 | 
					      value: '48hours',
 | 
				
			||||||
 | 
					      getTimeRange: () => {
 | 
				
			||||||
 | 
					        const endTime = getNow();
 | 
				
			||||||
 | 
					        const startTime = new Date(endTime.getTime() - 48 * 60 * 60 * 1000);
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					          startTime: getISOString(getUTCDate(startTime)),
 | 
				
			||||||
 | 
					          endTime: getISOString(getUTCDate(endTime)),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      label: '3 Days',
 | 
				
			||||||
 | 
					      value: '3days',
 | 
				
			||||||
 | 
					      getTimeRange: () => {
 | 
				
			||||||
 | 
					        const endTime = getNow();
 | 
				
			||||||
 | 
					        const startTime = new Date(endTime.getTime() - 3 * 24 * 60 * 60 * 1000);
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					          startTime: getISOString(getUTCDate(startTime)),
 | 
				
			||||||
 | 
					          endTime: getISOString(getUTCDate(endTime)),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      label: '1 Week',
 | 
				
			||||||
 | 
					      value: '1week',
 | 
				
			||||||
 | 
					      getTimeRange: () => {
 | 
				
			||||||
 | 
					        const endTime = getNow();
 | 
				
			||||||
 | 
					        const startTime = new Date(endTime.getTime() - 7 * 24 * 60 * 60 * 1000);
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					          startTime: getISOString(getUTCDate(startTime)),
 | 
				
			||||||
 | 
					          endTime: getISOString(getUTCDate(endTime)),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      label: '1 Month',
 | 
				
			||||||
 | 
					      value: '1month',
 | 
				
			||||||
 | 
					      getTimeRange: () => {
 | 
				
			||||||
 | 
					        const endTime = getNow();
 | 
				
			||||||
 | 
					        const startTime = new Date(endTime.getTime() - 30 * 24 * 60 * 60 * 1000); // Approximate month as 30 days
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					          startTime: getISOString(getUTCDate(startTime)),
 | 
				
			||||||
 | 
					          endTime: getISOString(getUTCDate(endTime)),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  ];
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Find a time range option by value
 | 
				
			||||||
 | 
					export const findTimeRangeOptionByValue = (value: string): TimeRangeOption | undefined => {
 | 
				
			||||||
 | 
					  return getTimeRangeOptions().find((option) => option.value === value);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Format date for display
 | 
				
			||||||
 | 
					export const formatDateTime = (isoString: string): string => {
 | 
				
			||||||
 | 
					  const date = new Date(isoString);
 | 
				
			||||||
 | 
					  return date.toLocaleString('en-US', {
 | 
				
			||||||
 | 
					    year: 'numeric',
 | 
				
			||||||
 | 
					    month: 'short',
 | 
				
			||||||
 | 
					    day: '2-digit',
 | 
				
			||||||
 | 
					    hour: '2-digit',
 | 
				
			||||||
 | 
					    minute: '2-digit',
 | 
				
			||||||
 | 
					    second: '2-digit',
 | 
				
			||||||
 | 
					    hour12: true,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										71
									
								
								src/lib/types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								src/lib/types.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,71 @@
 | 
				
			|||||||
 | 
					// Auth API Types
 | 
				
			||||||
 | 
					export interface RegisterRequest {
 | 
				
			||||||
 | 
					  username: string;
 | 
				
			||||||
 | 
					  password: string;
 | 
				
			||||||
 | 
					  email: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface RegisterResponse {
 | 
				
			||||||
 | 
					  success: boolean;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface LoginRequest {
 | 
				
			||||||
 | 
					  username: string;
 | 
				
			||||||
 | 
					  password: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface LoginResponse {
 | 
				
			||||||
 | 
					  success: boolean;
 | 
				
			||||||
 | 
					  wrongCredentials: boolean;
 | 
				
			||||||
 | 
					  token: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface ApiKeyResponse {
 | 
				
			||||||
 | 
					  success: boolean;
 | 
				
			||||||
 | 
					  apiKey: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// New ApiKey Type
 | 
				
			||||||
 | 
					export interface ApiKey {
 | 
				
			||||||
 | 
					  createdAt: string;
 | 
				
			||||||
 | 
					  key: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Feed API Types
 | 
				
			||||||
 | 
					export interface Feed {
 | 
				
			||||||
 | 
					  id: string;
 | 
				
			||||||
 | 
					  createdAt: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface AddFeedResponse {
 | 
				
			||||||
 | 
					  success: boolean;
 | 
				
			||||||
 | 
					  feedId: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface FeedDataPoint {
 | 
				
			||||||
 | 
					  timestamp: string;
 | 
				
			||||||
 | 
					  data: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Form Types
 | 
				
			||||||
 | 
					export interface AuthFormValues {
 | 
				
			||||||
 | 
					  username: string;
 | 
				
			||||||
 | 
					  password: string;
 | 
				
			||||||
 | 
					  email?: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Time Range Options
 | 
				
			||||||
 | 
					export interface TimeRangeOption {
 | 
				
			||||||
 | 
					  label: string;
 | 
				
			||||||
 | 
					  value: string;
 | 
				
			||||||
 | 
					  getTimeRange: () => {
 | 
				
			||||||
 | 
					    startTime: string;
 | 
				
			||||||
 | 
					    endTime: string;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Error Types
 | 
				
			||||||
 | 
					export interface ApiError {
 | 
				
			||||||
 | 
					  success: boolean;
 | 
				
			||||||
 | 
					  message: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										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))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										30
									
								
								src/main.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/main.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
				
			|||||||
 | 
					import * as React from "react";
 | 
				
			||||||
 | 
					import * as ReactDOM from "react-dom/client";
 | 
				
			||||||
 | 
					import { Toaster } from "sonner";
 | 
				
			||||||
 | 
					import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
 | 
				
			||||||
 | 
					import App from "./App";
 | 
				
			||||||
 | 
					import "./index.css";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Create a client for React Query
 | 
				
			||||||
 | 
					const queryClient = new QueryClient({
 | 
				
			||||||
 | 
					  defaultOptions: {
 | 
				
			||||||
 | 
					    queries: {
 | 
				
			||||||
 | 
					      retry: 1,
 | 
				
			||||||
 | 
					      refetchOnWindowFocus: false,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const rootElement = document.getElementById("root");
 | 
				
			||||||
 | 
					if (!rootElement) {
 | 
				
			||||||
 | 
					  throw new Error("Failed to find root element");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ReactDOM.createRoot(rootElement).render(
 | 
				
			||||||
 | 
					  <React.StrictMode>
 | 
				
			||||||
 | 
					    <QueryClientProvider client={queryClient}>
 | 
				
			||||||
 | 
					      <App />
 | 
				
			||||||
 | 
					      <Toaster position="top-right" />
 | 
				
			||||||
 | 
					    </QueryClientProvider>
 | 
				
			||||||
 | 
					  </React.StrictMode>
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
							
								
								
									
										190
									
								
								src/pages/ApiKeysPage.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										190
									
								
								src/pages/ApiKeysPage.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,190 @@
 | 
				
			|||||||
 | 
					import { useState, useEffect } from "react";
 | 
				
			||||||
 | 
					import { useNavigate } from "react-router-dom";
 | 
				
			||||||
 | 
					import { toast } from "sonner";
 | 
				
			||||||
 | 
					import { isAuthenticated } from "@/lib/auth";
 | 
				
			||||||
 | 
					import { createApiKey, getUserApiKeys, deleteApiKey } from "@/lib/api";
 | 
				
			||||||
 | 
					import { ApiKey } from "@/lib/types";
 | 
				
			||||||
 | 
					import { Button } from "@/components/ui/button";
 | 
				
			||||||
 | 
					import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
 | 
				
			||||||
 | 
					import { Separator } from "@/components/ui/separator";
 | 
				
			||||||
 | 
					import { Skeleton } from "@/components/ui/skeleton";
 | 
				
			||||||
 | 
					import { Alert, AlertDescription } from "@/components/ui/alert";
 | 
				
			||||||
 | 
					import { ApiKeyItem } from "@/components/ApiKeyItem";
 | 
				
			||||||
 | 
					import { AlertCircle, ExternalLink, Key, Loader2, Zap } from "lucide-react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function ApiKeysPage() {
 | 
				
			||||||
 | 
					  const navigate = useNavigate();
 | 
				
			||||||
 | 
					  const [apiKeys, setApiKeys] = useState<ApiKey[]>([]);
 | 
				
			||||||
 | 
					  const [isLoading, setIsLoading] = useState(true);
 | 
				
			||||||
 | 
					  const [isGenerating, setIsGenerating] = useState(false);
 | 
				
			||||||
 | 
					  const [error, setError] = useState<string | null>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Check if user is authenticated and fetch API keys
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (!isAuthenticated()) {
 | 
				
			||||||
 | 
					      toast.error("Please login to access API keys");
 | 
				
			||||||
 | 
					      navigate("/login");
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fetchApiKeys();
 | 
				
			||||||
 | 
					  }, [navigate]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const fetchApiKeys = async () => {
 | 
				
			||||||
 | 
					    setIsLoading(true);
 | 
				
			||||||
 | 
					    setError(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const keys = await getUserApiKeys();
 | 
				
			||||||
 | 
					      setApiKeys(keys);
 | 
				
			||||||
 | 
					    } catch (err: any) {
 | 
				
			||||||
 | 
					      setError(err.message || "Failed to fetch API keys");
 | 
				
			||||||
 | 
					    } finally {
 | 
				
			||||||
 | 
					      setIsLoading(false);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleGenerateApiKey = async () => {
 | 
				
			||||||
 | 
					    setIsGenerating(true);
 | 
				
			||||||
 | 
					    setError(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const response = await createApiKey();
 | 
				
			||||||
 | 
					      if (response.success) {
 | 
				
			||||||
 | 
					        toast.success("API key generated successfully");
 | 
				
			||||||
 | 
					        fetchApiKeys(); // Refresh the list
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        setError("Failed to generate API key");
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (err: any) {
 | 
				
			||||||
 | 
					      setError(err.message || "Failed to generate API key");
 | 
				
			||||||
 | 
					    } finally {
 | 
				
			||||||
 | 
					      setIsGenerating(false);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleDeleteApiKey = async (key: string) => {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      await deleteApiKey(key);
 | 
				
			||||||
 | 
					      await fetchApiKeys(); // Refresh the list after deletion
 | 
				
			||||||
 | 
					      return Promise.resolve();
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      return Promise.reject(error);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className="space-y-6">
 | 
				
			||||||
 | 
					      <div className="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
 | 
				
			||||||
 | 
					        <div>
 | 
				
			||||||
 | 
					          <h2 className="text-3xl font-bold tracking-tight">API Keys</h2>
 | 
				
			||||||
 | 
					          <p className="text-muted-foreground">
 | 
				
			||||||
 | 
					            Manage your API keys to integrate your IoT devices
 | 
				
			||||||
 | 
					          </p>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <Button
 | 
				
			||||||
 | 
					          variant="outline"
 | 
				
			||||||
 | 
					          size="sm"
 | 
				
			||||||
 | 
					          onClick={() => window.open("https://io-doc.monasefloadbalancer.ir", "_blank")}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <ExternalLink className="mr-2 h-4 w-4" />
 | 
				
			||||||
 | 
					          API Documentation
 | 
				
			||||||
 | 
					        </Button>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {error && (
 | 
				
			||||||
 | 
					        <Alert variant="destructive">
 | 
				
			||||||
 | 
					          <AlertCircle className="h-4 w-4" />
 | 
				
			||||||
 | 
					          <AlertDescription>{error}</AlertDescription>
 | 
				
			||||||
 | 
					        </Alert>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <Card>
 | 
				
			||||||
 | 
					        <CardHeader>
 | 
				
			||||||
 | 
					          <div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
 | 
				
			||||||
 | 
					            <div>
 | 
				
			||||||
 | 
					              <CardTitle>Your API Keys</CardTitle>
 | 
				
			||||||
 | 
					              <CardDescription>
 | 
				
			||||||
 | 
					                Use these keys to authenticate your IoT devices when sending data
 | 
				
			||||||
 | 
					              </CardDescription>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <Button
 | 
				
			||||||
 | 
					              onClick={handleGenerateApiKey}
 | 
				
			||||||
 | 
					              disabled={isGenerating}
 | 
				
			||||||
 | 
					              className="shrink-0"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              {isGenerating ? (
 | 
				
			||||||
 | 
					                <>
 | 
				
			||||||
 | 
					                  <Loader2 className="mr-2 h-4 w-4 animate-spin" />
 | 
				
			||||||
 | 
					                  Generating...
 | 
				
			||||||
 | 
					                </>
 | 
				
			||||||
 | 
					              ) : (
 | 
				
			||||||
 | 
					                <>
 | 
				
			||||||
 | 
					                  <Key className="mr-2 h-4 w-4" />
 | 
				
			||||||
 | 
					                  Generate New Key
 | 
				
			||||||
 | 
					                </>
 | 
				
			||||||
 | 
					              )}
 | 
				
			||||||
 | 
					            </Button>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <Separator />
 | 
				
			||||||
 | 
					        </CardHeader>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <CardContent className="space-y-4">
 | 
				
			||||||
 | 
					          {isLoading ? (
 | 
				
			||||||
 | 
					            <div className="space-y-3">
 | 
				
			||||||
 | 
					              <Skeleton className="h-20 w-full" />
 | 
				
			||||||
 | 
					              <Skeleton className="h-20 w-full" />
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          ) : apiKeys.length === 0 ? (
 | 
				
			||||||
 | 
					            <div className="flex flex-col items-center justify-center py-6 text-center">
 | 
				
			||||||
 | 
					              <Zap className="mb-2 h-10 w-10 text-muted-foreground" />
 | 
				
			||||||
 | 
					              <h3 className="text-lg font-medium">No API keys found</h3>
 | 
				
			||||||
 | 
					              <p className="text-sm text-muted-foreground mb-4">
 | 
				
			||||||
 | 
					                Generate a new API key to start sending data from your devices
 | 
				
			||||||
 | 
					              </p>
 | 
				
			||||||
 | 
					              <Button onClick={handleGenerateApiKey} disabled={isGenerating}>
 | 
				
			||||||
 | 
					                {isGenerating ? "Generating..." : "Generate API Key"}
 | 
				
			||||||
 | 
					              </Button>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          ) : (
 | 
				
			||||||
 | 
					            <>
 | 
				
			||||||
 | 
					              <div className="space-y-3">
 | 
				
			||||||
 | 
					                {apiKeys.map((key) => (
 | 
				
			||||||
 | 
					                  <ApiKeyItem
 | 
				
			||||||
 | 
					                    key={key.key}
 | 
				
			||||||
 | 
					                    apiKey={key}
 | 
				
			||||||
 | 
					                    onDelete={handleDeleteApiKey}
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                ))}
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <Alert>
 | 
				
			||||||
 | 
					                <AlertCircle className="h-4 w-4" />
 | 
				
			||||||
 | 
					                <AlertDescription>
 | 
				
			||||||
 | 
					                  Keep your API keys secure. Anyone with your API key can send data to your feeds.
 | 
				
			||||||
 | 
					                  If a key is compromised, delete it and generate a new one.
 | 
				
			||||||
 | 
					                </AlertDescription>
 | 
				
			||||||
 | 
					              </Alert>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <div className="border rounded-md p-4">
 | 
				
			||||||
 | 
					                <h3 className="font-medium mb-2 flex items-center">
 | 
				
			||||||
 | 
					                  API Documentation
 | 
				
			||||||
 | 
					                  <ExternalLink className="ml-1 h-3 w-3" />
 | 
				
			||||||
 | 
					                </h3>
 | 
				
			||||||
 | 
					                <p className="text-sm text-muted-foreground mb-3">
 | 
				
			||||||
 | 
					                  For detailed information on how to use the API, please visit our documentation.
 | 
				
			||||||
 | 
					                </p>
 | 
				
			||||||
 | 
					                <Button
 | 
				
			||||||
 | 
					                  variant="outline"
 | 
				
			||||||
 | 
					                  onClick={() => window.open("https://io-doc.monasefloadbalancer.ir", "_blank")}
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  Visit Documentation
 | 
				
			||||||
 | 
					                </Button>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </>
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
 | 
					        </CardContent>
 | 
				
			||||||
 | 
					      </Card>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										257
									
								
								src/pages/DashboardPage.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										257
									
								
								src/pages/DashboardPage.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,257 @@
 | 
				
			|||||||
 | 
					import { useEffect, useState } from "react";
 | 
				
			||||||
 | 
					import { useNavigate } from "react-router-dom";
 | 
				
			||||||
 | 
					import { toast } from "sonner";
 | 
				
			||||||
 | 
					import { isAuthenticated } from "@/lib/auth";
 | 
				
			||||||
 | 
					import { useFeedStore } from "@/lib/store";
 | 
				
			||||||
 | 
					import { addNewFeed } from "@/lib/api";
 | 
				
			||||||
 | 
					import { formatDateTime, getTimeRangeOptions } from "@/lib/timeRanges";
 | 
				
			||||||
 | 
					import { Button } from "@/components/ui/button";
 | 
				
			||||||
 | 
					import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  Select,
 | 
				
			||||||
 | 
					  SelectContent,
 | 
				
			||||||
 | 
					  SelectItem,
 | 
				
			||||||
 | 
					  SelectTrigger,
 | 
				
			||||||
 | 
					  SelectValue,
 | 
				
			||||||
 | 
					} from "@/components/ui/select";
 | 
				
			||||||
 | 
					import { Skeleton } from "@/components/ui/skeleton";
 | 
				
			||||||
 | 
					import { Plus, RefreshCw, ScanBarcode } from "lucide-react";
 | 
				
			||||||
 | 
					import { FeedChart } from "@/components/FeedChart";
 | 
				
			||||||
 | 
					import { FeedActions } from "@/components/FeedActions";
 | 
				
			||||||
 | 
					import { Alert, AlertDescription } from "@/components/ui/alert";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function DashboardPage() {
 | 
				
			||||||
 | 
					  const navigate = useNavigate();
 | 
				
			||||||
 | 
					  const [timeRangeValue, setTimeRangeValue] = useState("30min");
 | 
				
			||||||
 | 
					  const {
 | 
				
			||||||
 | 
					    feeds,
 | 
				
			||||||
 | 
					    selectedFeed,
 | 
				
			||||||
 | 
					    feedData,
 | 
				
			||||||
 | 
					    isLoading,
 | 
				
			||||||
 | 
					    error,
 | 
				
			||||||
 | 
					    fetchFeeds,
 | 
				
			||||||
 | 
					    selectFeed,
 | 
				
			||||||
 | 
					    setTimeRange,
 | 
				
			||||||
 | 
					    refreshCurrentFeedData,
 | 
				
			||||||
 | 
					    deleteFeed,
 | 
				
			||||||
 | 
					  } = useFeedStore();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Check if user is authenticated
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (!isAuthenticated()) {
 | 
				
			||||||
 | 
					      toast.error("Please login to access the dashboard");
 | 
				
			||||||
 | 
					      navigate("/login");
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Fetch feeds on component mount
 | 
				
			||||||
 | 
					    fetchFeeds();
 | 
				
			||||||
 | 
					  }, [navigate, fetchFeeds]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Handle time range change
 | 
				
			||||||
 | 
					  const handleTimeRangeChange = (value: string) => {
 | 
				
			||||||
 | 
					    setTimeRangeValue(value);
 | 
				
			||||||
 | 
					    const selectedOption = getTimeRangeOptions().find(option => option.value === value);
 | 
				
			||||||
 | 
					    if (selectedOption) {
 | 
				
			||||||
 | 
					      const { startTime, endTime } = selectedOption.getTimeRange();
 | 
				
			||||||
 | 
					      setTimeRange(startTime, endTime);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Handle add new feed
 | 
				
			||||||
 | 
					  const handleAddNewFeed = async () => {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const response = await addNewFeed();
 | 
				
			||||||
 | 
					      if (response.success) {
 | 
				
			||||||
 | 
					        toast.success("New feed created successfully");
 | 
				
			||||||
 | 
					        fetchFeeds();
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        toast.error("Failed to create new feed");
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      toast.error("An error occurred while creating a new feed");
 | 
				
			||||||
 | 
					      console.error(error);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Handle refresh
 | 
				
			||||||
 | 
					  const handleRefresh = async () => {
 | 
				
			||||||
 | 
					    // First refresh the feeds list
 | 
				
			||||||
 | 
					    await fetchFeeds();
 | 
				
			||||||
 | 
					    // Then refresh current feed data if a feed is selected
 | 
				
			||||||
 | 
					    if (selectedFeed) {
 | 
				
			||||||
 | 
					      await refreshCurrentFeedData();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    toast.success("Data refreshed");
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Handle delete feed
 | 
				
			||||||
 | 
					  const handleDeleteFeed = async (feedId: string) => {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      await deleteFeed(feedId);
 | 
				
			||||||
 | 
					      toast.success("Feed deleted successfully");
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      toast.error("Failed to delete feed");
 | 
				
			||||||
 | 
					      console.error(error);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className="space-y-6">
 | 
				
			||||||
 | 
					      <div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
 | 
				
			||||||
 | 
					        <div>
 | 
				
			||||||
 | 
					          <h2 className="text-3xl font-bold tracking-tight">Dashboard</h2>
 | 
				
			||||||
 | 
					          <p className="text-muted-foreground">
 | 
				
			||||||
 | 
					            Monitor and visualize your IoT device data
 | 
				
			||||||
 | 
					          </p>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div className="flex items-center gap-2">
 | 
				
			||||||
 | 
					          {/* Button to add a new feed */}
 | 
				
			||||||
 | 
					          <Button size="sm" onClick={handleAddNewFeed} disabled={isLoading}>
 | 
				
			||||||
 | 
					            Add Feed
 | 
				
			||||||
 | 
					            <Plus className="ml-2 h-4 w-4" />
 | 
				
			||||||
 | 
					          </Button>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {error && (
 | 
				
			||||||
 | 
					        <Alert variant="destructive">
 | 
				
			||||||
 | 
					          <AlertDescription>{error}</AlertDescription>
 | 
				
			||||||
 | 
					        </Alert>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div className="grid gap-6 md:grid-cols-4">
 | 
				
			||||||
 | 
					        <Card className="md:col-span-1 relative">
 | 
				
			||||||
 | 
					          <button 
 | 
				
			||||||
 | 
					            className="absolute top-3 right-3 p-1 border border-gray-300 rounded-md shadow-sm hover:shadow-md focus:outline-none focus:ring-2 focus:ring-blue-500" 
 | 
				
			||||||
 | 
					            onClick={fetchFeeds} 
 | 
				
			||||||
 | 
					            disabled={isLoading}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <RefreshCw className="h-5 w-5" />
 | 
				
			||||||
 | 
					          </button>
 | 
				
			||||||
 | 
					          <CardHeader>
 | 
				
			||||||
 | 
					            <CardTitle>Feeds</CardTitle>
 | 
				
			||||||
 | 
					            <CardDescription>Your IoT data feeds</CardDescription>
 | 
				
			||||||
 | 
					          </CardHeader>
 | 
				
			||||||
 | 
					          <CardContent>
 | 
				
			||||||
 | 
					            {isLoading ? (
 | 
				
			||||||
 | 
					              <div className="space-y-2">
 | 
				
			||||||
 | 
					                <Skeleton className="h-10 w-full" />
 | 
				
			||||||
 | 
					                <Skeleton className="h-10 w-full" />
 | 
				
			||||||
 | 
					                <Skeleton className="h-10 w-full" />
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            ) : feeds.length === 0 ? (
 | 
				
			||||||
 | 
					              <div className="flex flex-col items-center justify-center py-6 text-center">
 | 
				
			||||||
 | 
					                <ScanBarcode className="mb-2 h-10 w-10 text-muted-foreground" />
 | 
				
			||||||
 | 
					                <h3 className="text-lg font-medium">No feeds found</h3>
 | 
				
			||||||
 | 
					                <p className="text-sm text-muted-foreground mb-4">
 | 
				
			||||||
 | 
					                  Create a new feed to start collecting data
 | 
				
			||||||
 | 
					                </p>
 | 
				
			||||||
 | 
					                <Button onClick={handleAddNewFeed}>
 | 
				
			||||||
 | 
					                  <Plus className="mr-2 h-4 w-4" />
 | 
				
			||||||
 | 
					                  Add Feed
 | 
				
			||||||
 | 
					                </Button>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            ) : (
 | 
				
			||||||
 | 
					              <div className="space-y-2">
 | 
				
			||||||
 | 
					                {feeds.map((feed) => (
 | 
				
			||||||
 | 
					                  <div
 | 
				
			||||||
 | 
					                    key={feed.id}
 | 
				
			||||||
 | 
					                    className="flex items-center gap-1"
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    <Button
 | 
				
			||||||
 | 
					                      variant={selectedFeed?.id === feed.id ? "default" : "outline"}
 | 
				
			||||||
 | 
					                      className="w-full justify-start text-left flex-grow overflow-hidden"
 | 
				
			||||||
 | 
					                      onClick={() => selectFeed(feed)}
 | 
				
			||||||
 | 
					                    >
 | 
				
			||||||
 | 
					                      <div className="flex flex-col items-start w-full overflow-hidden">
 | 
				
			||||||
 | 
					                        <div className="font-medium truncate w-full">
 | 
				
			||||||
 | 
					                          Feed {feed.id.substring(0, 8)}...
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                        <div className="text-xs text-muted-foreground truncate w-full">
 | 
				
			||||||
 | 
					                          Created: {formatDateTime(feed.createdAt)}
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                      </div>
 | 
				
			||||||
 | 
					                    </Button>
 | 
				
			||||||
 | 
					                    <FeedActions feed={feed} onDelete={handleDeleteFeed} />
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                ))}
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					          </CardContent>
 | 
				
			||||||
 | 
					        </Card>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <Card className="md:col-span-3 relative">
 | 
				
			||||||
 | 
					  <CardHeader className="flex flex-col md:flex-row md:items-center md:justify-between space-y-2 md:space-y-0">
 | 
				
			||||||
 | 
					    <div>
 | 
				
			||||||
 | 
					      <CardTitle>Feed Data</CardTitle>
 | 
				
			||||||
 | 
					      <CardDescription>
 | 
				
			||||||
 | 
					        {selectedFeed
 | 
				
			||||||
 | 
					          ? `Showing data for Feed ${selectedFeed.id.substring(0, 8)}...`
 | 
				
			||||||
 | 
					          : "Select a feed to view data"
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      </CardDescription>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    {/* Flex container for the select box and button */}
 | 
				
			||||||
 | 
					    <div className="flex items-center space-x-2">
 | 
				
			||||||
 | 
					      <Select
 | 
				
			||||||
 | 
					        value={timeRangeValue}
 | 
				
			||||||
 | 
					        onValueChange={handleTimeRangeChange}
 | 
				
			||||||
 | 
					        disabled={!selectedFeed || isLoading}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <SelectTrigger className="w-[180px]">
 | 
				
			||||||
 | 
					          <SelectValue placeholder="Select time range" />
 | 
				
			||||||
 | 
					        </SelectTrigger>
 | 
				
			||||||
 | 
					        <SelectContent>
 | 
				
			||||||
 | 
					          {getTimeRangeOptions().map((option) => (
 | 
				
			||||||
 | 
					            <SelectItem key={option.value} value={option.value}>
 | 
				
			||||||
 | 
					              {option.label}
 | 
				
			||||||
 | 
					            </SelectItem>
 | 
				
			||||||
 | 
					          ))}
 | 
				
			||||||
 | 
					        </SelectContent>
 | 
				
			||||||
 | 
					      </Select>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {/* Button to refresh feed data */}
 | 
				
			||||||
 | 
					      <button
 | 
				
			||||||
 | 
					        className={`flex items-center p-1 border rounded-md 
 | 
				
			||||||
 | 
					          ${isLoading || !selectedFeed 
 | 
				
			||||||
 | 
					            ? "border-gray-300 text-gray-400 bg-gray-200" 
 | 
				
			||||||
 | 
					            : "border-blue-500 text-blue-700 hover:bg-blue-100"}
 | 
				
			||||||
 | 
					          shadow-sm hover:shadow-md focus:outline-none focus:ring-2 focus:ring-blue-500`}
 | 
				
			||||||
 | 
					        onClick={() => selectedFeed && refreshCurrentFeedData()}
 | 
				
			||||||
 | 
					        disabled={isLoading || !selectedFeed}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <RefreshCw className="h-5 w-5" />
 | 
				
			||||||
 | 
					      </button>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </CardHeader>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <CardContent className="pt-6">
 | 
				
			||||||
 | 
					    {isLoading ? (
 | 
				
			||||||
 | 
					      <Skeleton className="h-[350px] w-full" />
 | 
				
			||||||
 | 
					    ) : !selectedFeed ? (
 | 
				
			||||||
 | 
					      <div className="flex flex-col items-center justify-center h-[350px] text-center">
 | 
				
			||||||
 | 
					        <ScanBarcode className="mb-2 h-10 w-10 text-muted-foreground" />
 | 
				
			||||||
 | 
					        <h3 className="text-lg font-medium">No feed selected</h3>
 | 
				
			||||||
 | 
					        <p className="text-sm text-muted-foreground">
 | 
				
			||||||
 | 
					          Select a feed from the sidebar to view its data
 | 
				
			||||||
 | 
					        </p>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    ) : feedData.length === 0 ? (
 | 
				
			||||||
 | 
					      <div className="flex flex-col items-center justify-center h-[350px] text-center">
 | 
				
			||||||
 | 
					        <ScanBarcode className="mb-2 h-10 w-10 text-muted-foreground" />
 | 
				
			||||||
 | 
					        <h3 className="text-lg font-medium">No data available</h3>
 | 
				
			||||||
 | 
					        <p className="text-sm text-muted-foreground">
 | 
				
			||||||
 | 
					          This feed has no data for the selected time range
 | 
				
			||||||
 | 
					        </p>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    ) : (
 | 
				
			||||||
 | 
					      <FeedChart data={feedData} />
 | 
				
			||||||
 | 
					    )}
 | 
				
			||||||
 | 
					  </CardContent>
 | 
				
			||||||
 | 
					</Card>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										85
									
								
								src/pages/HomePage.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								src/pages/HomePage.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,85 @@
 | 
				
			|||||||
 | 
					import { useNavigate } from "react-router-dom";
 | 
				
			||||||
 | 
					import { Button } from "@/components/ui/button";
 | 
				
			||||||
 | 
					import { isAuthenticated } from "@/lib/auth";
 | 
				
			||||||
 | 
					import { ExternalLink, ArrowRight, LineChart, ShieldCheck, Zap, Bold } from "lucide-react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function HomePage() {
 | 
				
			||||||
 | 
					  const navigate = useNavigate();
 | 
				
			||||||
 | 
					  const authenticated = isAuthenticated();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className="flex flex-col gap-12">
 | 
				
			||||||
 | 
					      <section className="pt-12 pb-16 md:pt-20 md:pb-28">
 | 
				
			||||||
 | 
					        <div className="text-center space-y-6">
 | 
				
			||||||
 | 
					          <h1 className="text-4xl md:text-6xl font-bold tracking-tighter">
 | 
				
			||||||
 | 
					            IoT Plotter
 | 
				
			||||||
 | 
					            <span className="text-primary">.</span>
 | 
				
			||||||
 | 
					          </h1>
 | 
				
			||||||
 | 
					          <p className="text-xl md:text-2xl text-muted-foreground max-w-3xl mx-auto">
 | 
				
			||||||
 | 
					            Monitor, visualize, and analyze your IoT data in real-time with our easy-to-use platform
 | 
				
			||||||
 | 
					          </p>
 | 
				
			||||||
 | 
					          <div className="flex flex-wrap justify-center gap-4 pt-4">
 | 
				
			||||||
 | 
					            <Button
 | 
				
			||||||
 | 
					              size="lg"
 | 
				
			||||||
 | 
					              onClick={() => navigate(authenticated ? "/dashboard" : "/register")}
 | 
				
			||||||
 | 
					              className="h-12 px-6"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              {authenticated ? "Dashboard" : "Get Started"}{" "}
 | 
				
			||||||
 | 
					              <ArrowRight className="ml-2 h-4 w-4" />
 | 
				
			||||||
 | 
					            </Button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            {!authenticated && (
 | 
				
			||||||
 | 
					              <Button
 | 
				
			||||||
 | 
					                variant="outline"
 | 
				
			||||||
 | 
					                size="lg"
 | 
				
			||||||
 | 
					                onClick={() => navigate("/login")}
 | 
				
			||||||
 | 
					                className="h-12 px-6"
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                Login
 | 
				
			||||||
 | 
					              </Button>
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <Button
 | 
				
			||||||
 | 
					              variant="outline"
 | 
				
			||||||
 | 
					              size="sm"
 | 
				
			||||||
 | 
					              onClick={() => window.open("https://io-doc.monasefloadbalancer.ir", "_blank")}
 | 
				
			||||||
 | 
					              className="h-12"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <ExternalLink className="mr-2 h-6 w-6" />
 | 
				
			||||||
 | 
					              API Documentation
 | 
				
			||||||
 | 
					            </Button>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </section>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <section className="py-10">
 | 
				
			||||||
 | 
					        <div className="grid gap-8 md:grid-cols-3">
 | 
				
			||||||
 | 
					          <div className="flex flex-col items-center text-center p-6 rounded-lg border bg-card shadow-sm">
 | 
				
			||||||
 | 
					            <LineChart className="h-12 w-12 text-primary mb-4" />
 | 
				
			||||||
 | 
					            <h3 className="text-xl font-semibold mb-2">Real-time Visualization</h3>
 | 
				
			||||||
 | 
					            <p className="text-muted-foreground">
 | 
				
			||||||
 | 
					              Monitor your IoT device data in real-time with interactive charts and customizable
 | 
				
			||||||
 | 
					              dashboards
 | 
				
			||||||
 | 
					            </p>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <div className="flex flex-col items-center text-center p-6 rounded-lg border bg-card shadow-sm">
 | 
				
			||||||
 | 
					            <ShieldCheck className="h-12 w-12 text-primary mb-4" />
 | 
				
			||||||
 | 
					            <h3 className="text-xl font-semibold mb-2">Secure Communication</h3>
 | 
				
			||||||
 | 
					            <p className="text-muted-foreground">
 | 
				
			||||||
 | 
					              Industry-standard encryption and authentication for all your IoT communications
 | 
				
			||||||
 | 
					            </p>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <div className="flex flex-col items-center text-center p-6 rounded-lg border bg-card shadow-sm">
 | 
				
			||||||
 | 
					            <Zap className="h-12 w-12 text-primary mb-4" />
 | 
				
			||||||
 | 
					            <h3 className="text-xl font-semibold mb-2">Simple Integration</h3>
 | 
				
			||||||
 | 
					            <p className="text-muted-foreground">
 | 
				
			||||||
 | 
					              Easy-to-use API with comprehensive documentation for quick integration with any device
 | 
				
			||||||
 | 
					            </p>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </section>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										138
									
								
								src/pages/LoginPage.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								src/pages/LoginPage.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,138 @@
 | 
				
			|||||||
 | 
					import { useState } from "react";
 | 
				
			||||||
 | 
					import { useNavigate } from "react-router-dom";
 | 
				
			||||||
 | 
					import { useForm } from "react-hook-form";
 | 
				
			||||||
 | 
					import { z } from "zod";
 | 
				
			||||||
 | 
					import { zodResolver } from "@hookform/resolvers/zod";
 | 
				
			||||||
 | 
					import { toast } from "sonner";
 | 
				
			||||||
 | 
					import { loginUser } from "@/lib/api";
 | 
				
			||||||
 | 
					import { setToken } from "@/lib/auth";
 | 
				
			||||||
 | 
					import { Button } from "@/components/ui/button";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  Card,
 | 
				
			||||||
 | 
					  CardContent,
 | 
				
			||||||
 | 
					  CardDescription,
 | 
				
			||||||
 | 
					  CardHeader,
 | 
				
			||||||
 | 
					  CardTitle,
 | 
				
			||||||
 | 
					} from "@/components/ui/card";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  Form,
 | 
				
			||||||
 | 
					  FormControl,
 | 
				
			||||||
 | 
					  FormField,
 | 
				
			||||||
 | 
					  FormItem,
 | 
				
			||||||
 | 
					  FormLabel,
 | 
				
			||||||
 | 
					  FormMessage,
 | 
				
			||||||
 | 
					} from "@/components/ui/form";
 | 
				
			||||||
 | 
					import { Input } from "@/components/ui/input";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Schema for login form validation
 | 
				
			||||||
 | 
					const formSchema = z.object({
 | 
				
			||||||
 | 
					  username: z.string().min(1, { message: "Username is required" }),
 | 
				
			||||||
 | 
					  password: z.string().min(1, { message: "Password is required" }),
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type FormValues = z.infer<typeof formSchema>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function LoginPage() {
 | 
				
			||||||
 | 
					  const navigate = useNavigate();
 | 
				
			||||||
 | 
					  const [isLoading, setIsLoading] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const form = useForm<FormValues>({
 | 
				
			||||||
 | 
					    resolver: zodResolver(formSchema),
 | 
				
			||||||
 | 
					    defaultValues: {
 | 
				
			||||||
 | 
					      username: "",
 | 
				
			||||||
 | 
					      password: "",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const onSubmit = async (data: FormValues) => {
 | 
				
			||||||
 | 
					    setIsLoading(true);
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const response = await loginUser(data.username, data.password);
 | 
				
			||||||
 | 
					      if (response.success) {
 | 
				
			||||||
 | 
					        // Save token and proceed if login successful
 | 
				
			||||||
 | 
					        setToken(response.token);
 | 
				
			||||||
 | 
					        toast.success("Login successful");
 | 
				
			||||||
 | 
					        navigate("/dashboard");
 | 
				
			||||||
 | 
					      } else if (response.emailNotVerified) {
 | 
				
			||||||
 | 
					        // If email is not verified, show error message
 | 
				
			||||||
 | 
					        toast.error(
 | 
				
			||||||
 | 
					          "Your email is not verified. Please check your inbox (including spam) for the verification link."
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        toast.error(response.message || "Invalid username or password");
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (error: any) {
 | 
				
			||||||
 | 
					      console.error(error);
 | 
				
			||||||
 | 
					      toast.error(error.message || "An error occurred during login");
 | 
				
			||||||
 | 
					    } finally {
 | 
				
			||||||
 | 
					      setIsLoading(false);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className="flex justify-center items-center py-10">
 | 
				
			||||||
 | 
					      <Card className="rounded-xl w-full max-w-md shadow-lg">
 | 
				
			||||||
 | 
					        <CardHeader>
 | 
				
			||||||
 | 
					          <CardTitle className="tracking-tight text-2xl">
 | 
				
			||||||
 | 
					            Login to your account
 | 
				
			||||||
 | 
					          </CardTitle>
 | 
				
			||||||
 | 
					          <CardDescription>
 | 
				
			||||||
 | 
					            Enter your credentials to access your account
 | 
				
			||||||
 | 
					          </CardDescription>
 | 
				
			||||||
 | 
					        </CardHeader>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <CardContent>
 | 
				
			||||||
 | 
					          <Form {...form}>
 | 
				
			||||||
 | 
					            <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
 | 
				
			||||||
 | 
					              <FormField
 | 
				
			||||||
 | 
					                control={form.control}
 | 
				
			||||||
 | 
					                name="username"
 | 
				
			||||||
 | 
					                render={({ field }) => (
 | 
				
			||||||
 | 
					                  <FormItem>
 | 
				
			||||||
 | 
					                    <FormLabel>Username</FormLabel>
 | 
				
			||||||
 | 
					                    <FormControl>
 | 
				
			||||||
 | 
					                      <Input placeholder="johndoe" {...field} />
 | 
				
			||||||
 | 
					                    </FormControl>
 | 
				
			||||||
 | 
					                    <FormMessage />
 | 
				
			||||||
 | 
					                  </FormItem>
 | 
				
			||||||
 | 
					                )}
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <FormField
 | 
				
			||||||
 | 
					                control={form.control}
 | 
				
			||||||
 | 
					                name="password"
 | 
				
			||||||
 | 
					                render={({ field }) => (
 | 
				
			||||||
 | 
					                  <FormItem>
 | 
				
			||||||
 | 
					                    <FormLabel>Password</FormLabel>
 | 
				
			||||||
 | 
					                    <FormControl>
 | 
				
			||||||
 | 
					                      <Input type="password" placeholder="********" {...field} />
 | 
				
			||||||
 | 
					                    </FormControl>
 | 
				
			||||||
 | 
					                    <FormMessage />
 | 
				
			||||||
 | 
					                  </FormItem>
 | 
				
			||||||
 | 
					                )}
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <Button
 | 
				
			||||||
 | 
					                type="submit"
 | 
				
			||||||
 | 
					                className="w-full"
 | 
				
			||||||
 | 
					                disabled={isLoading}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                {isLoading ? "Logging in..." : "Login"}
 | 
				
			||||||
 | 
					              </Button>
 | 
				
			||||||
 | 
					            </form>
 | 
				
			||||||
 | 
					          </Form>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <div className="mt-4 text-center">
 | 
				
			||||||
 | 
					            <p className="text-sm text-muted-foreground">
 | 
				
			||||||
 | 
					              Don't have an account?{" "}
 | 
				
			||||||
 | 
					              <a href="/register" className="text-primary hover:underline">
 | 
				
			||||||
 | 
					                Register
 | 
				
			||||||
 | 
					              </a>
 | 
				
			||||||
 | 
					            </p>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </CardContent>
 | 
				
			||||||
 | 
					      </Card>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										85
									
								
								src/pages/ProfilePage.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								src/pages/ProfilePage.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,85 @@
 | 
				
			|||||||
 | 
					import { useEffect } from "react";
 | 
				
			||||||
 | 
					import { useNavigate } from "react-router-dom";
 | 
				
			||||||
 | 
					import { toast } from "sonner";
 | 
				
			||||||
 | 
					import { isAuthenticated, getUsername, clearToken } from "@/lib/auth";
 | 
				
			||||||
 | 
					import { Button } from "@/components/ui/button";
 | 
				
			||||||
 | 
					import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
 | 
				
			||||||
 | 
					import { Avatar, AvatarFallback } from "@/components/ui/avatar";
 | 
				
			||||||
 | 
					import { LogOut, UserCircle } from "lucide-react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function ProfilePage() {
 | 
				
			||||||
 | 
					  const navigate = useNavigate();
 | 
				
			||||||
 | 
					  const username = getUsername();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Check if user is authenticated
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (!isAuthenticated()) {
 | 
				
			||||||
 | 
					      toast.error("Please login to view your profile");
 | 
				
			||||||
 | 
					      navigate("/login");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [navigate]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleLogout = () => {
 | 
				
			||||||
 | 
					    clearToken();
 | 
				
			||||||
 | 
					    toast.success("You have been logged out");
 | 
				
			||||||
 | 
					    navigate("/login");
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const getUserInitials = () => {
 | 
				
			||||||
 | 
					    if (!username) return "U";
 | 
				
			||||||
 | 
					    return username.substring(0, 2).toUpperCase();
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className="space-y-6">
 | 
				
			||||||
 | 
					      <div>
 | 
				
			||||||
 | 
					        <h2 className="text-3xl font-bold tracking-tight">Profile</h2>
 | 
				
			||||||
 | 
					        <p className="text-muted-foreground">
 | 
				
			||||||
 | 
					          Manage your account settings
 | 
				
			||||||
 | 
					        </p>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <Card>
 | 
				
			||||||
 | 
					        <CardHeader>
 | 
				
			||||||
 | 
					          <CardTitle>Your Profile</CardTitle>
 | 
				
			||||||
 | 
					          <CardDescription>
 | 
				
			||||||
 | 
					            View and manage your account information
 | 
				
			||||||
 | 
					          </CardDescription>
 | 
				
			||||||
 | 
					        </CardHeader>
 | 
				
			||||||
 | 
					        <CardContent className="space-y-6">
 | 
				
			||||||
 | 
					          <div className="flex items-center gap-4">
 | 
				
			||||||
 | 
					            <Avatar className="h-20 w-20">
 | 
				
			||||||
 | 
					              <AvatarFallback className="text-3xl">{getUserInitials()}</AvatarFallback>
 | 
				
			||||||
 | 
					            </Avatar>
 | 
				
			||||||
 | 
					            <div>
 | 
				
			||||||
 | 
					              <h3 className="text-xl font-medium">{username}</h3>
 | 
				
			||||||
 | 
					              <p className="text-sm text-muted-foreground">IoT Plotter User</p>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <div className="border-t pt-6">
 | 
				
			||||||
 | 
					            <h3 className="font-medium mb-4">Account Actions</h3>
 | 
				
			||||||
 | 
					            <div className="space-y-2">
 | 
				
			||||||
 | 
					              <Button
 | 
				
			||||||
 | 
					                variant="outline"
 | 
				
			||||||
 | 
					                className="w-full justify-start"
 | 
				
			||||||
 | 
					                onClick={() => navigate("/api-keys")}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <UserCircle className="mr-2 h-4 w-4" />
 | 
				
			||||||
 | 
					                Manage API Keys
 | 
				
			||||||
 | 
					              </Button>
 | 
				
			||||||
 | 
					              <Button
 | 
				
			||||||
 | 
					                variant="outline"
 | 
				
			||||||
 | 
					                className="w-full justify-start text-destructive hover:text-destructive"
 | 
				
			||||||
 | 
					                onClick={handleLogout}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <LogOut className="mr-2 h-4 w-4" />
 | 
				
			||||||
 | 
					                Logout
 | 
				
			||||||
 | 
					              </Button>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </CardContent>
 | 
				
			||||||
 | 
					      </Card>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										166
									
								
								src/pages/RegisterPage.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								src/pages/RegisterPage.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,166 @@
 | 
				
			|||||||
 | 
					import { useState } from "react";
 | 
				
			||||||
 | 
					import { useNavigate } from "react-router-dom";
 | 
				
			||||||
 | 
					import { useForm } from "react-hook-form";
 | 
				
			||||||
 | 
					import { z } from "zod";
 | 
				
			||||||
 | 
					import { zodResolver } from "@hookform/resolvers/zod";
 | 
				
			||||||
 | 
					import { toast } from "sonner";
 | 
				
			||||||
 | 
					import { registerUser } from "@/lib/api";
 | 
				
			||||||
 | 
					import { setToken } from "@/lib/auth";
 | 
				
			||||||
 | 
					import { Button } from "@/components/ui/button";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  Card,
 | 
				
			||||||
 | 
					  CardContent,
 | 
				
			||||||
 | 
					  CardDescription,
 | 
				
			||||||
 | 
					  CardHeader,
 | 
				
			||||||
 | 
					  CardTitle,
 | 
				
			||||||
 | 
					} from "@/components/ui/card";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  Form,
 | 
				
			||||||
 | 
					  FormControl,
 | 
				
			||||||
 | 
					  FormField,
 | 
				
			||||||
 | 
					  FormItem,
 | 
				
			||||||
 | 
					  FormLabel,
 | 
				
			||||||
 | 
					  FormMessage,
 | 
				
			||||||
 | 
					} from "@/components/ui/form";
 | 
				
			||||||
 | 
					import { Input } from "@/components/ui/input";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Schema for register form validation
 | 
				
			||||||
 | 
					const formSchema = z
 | 
				
			||||||
 | 
					  .object({
 | 
				
			||||||
 | 
					    username: z.string().min(3, { message: "Username must be at least 3 characters" }),
 | 
				
			||||||
 | 
					    email: z.string().email({ message: "Invalid email address" }),
 | 
				
			||||||
 | 
					    password: z.string().min(8, { message: "Password must be at least 8 characters" }),
 | 
				
			||||||
 | 
					    confirmPassword: z.string(),
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  .refine((data) => data.password === data.confirmPassword, {
 | 
				
			||||||
 | 
					    message: "Passwords do not match",
 | 
				
			||||||
 | 
					    path: ["confirmPassword"],
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type FormValues = z.infer<typeof formSchema>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function RegisterPage() {
 | 
				
			||||||
 | 
					  const navigate = useNavigate();
 | 
				
			||||||
 | 
					  const [isLoading, setIsLoading] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const form = useForm<FormValues>({
 | 
				
			||||||
 | 
					    resolver: zodResolver(formSchema),
 | 
				
			||||||
 | 
					    defaultValues: {
 | 
				
			||||||
 | 
					      username: "",
 | 
				
			||||||
 | 
					      email: "",
 | 
				
			||||||
 | 
					      password: "",
 | 
				
			||||||
 | 
					      confirmPassword: "",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const onSubmit = async (data: FormValues) => {
 | 
				
			||||||
 | 
					    setIsLoading(true);
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const response = await registerUser(data.username, data.password, data.email);
 | 
				
			||||||
 | 
					      if (response.success) {
 | 
				
			||||||
 | 
					        toast.success("Registration successful! Check your email (including spam) for the verification link.");
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        toast.error(response.message || "Registration failed. Please try again.");
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (error: any) {
 | 
				
			||||||
 | 
					      console.error(error);
 | 
				
			||||||
 | 
					      toast.error(error.message || "An error occurred during registration.");
 | 
				
			||||||
 | 
					    } finally {
 | 
				
			||||||
 | 
					      setIsLoading(false);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className="flex justify-center items-center py-10">
 | 
				
			||||||
 | 
					      <Card className="rounded-xl w-full max-w-md shadow-lg">
 | 
				
			||||||
 | 
					        <CardHeader>
 | 
				
			||||||
 | 
					          <CardTitle className="tracking-tight text-2xl">
 | 
				
			||||||
 | 
					            Create an account
 | 
				
			||||||
 | 
					          </CardTitle>
 | 
				
			||||||
 | 
					          <CardDescription>
 | 
				
			||||||
 | 
					            Register to start monitoring your IoT data
 | 
				
			||||||
 | 
					          </CardDescription>
 | 
				
			||||||
 | 
					        </CardHeader>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <CardContent>
 | 
				
			||||||
 | 
					          <Form {...form}>
 | 
				
			||||||
 | 
					            <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
 | 
				
			||||||
 | 
					              <FormField
 | 
				
			||||||
 | 
					                control={form.control}
 | 
				
			||||||
 | 
					                name="username"
 | 
				
			||||||
 | 
					                render={({ field }) => (
 | 
				
			||||||
 | 
					                  <FormItem>
 | 
				
			||||||
 | 
					                    <FormLabel>Username</FormLabel>
 | 
				
			||||||
 | 
					                    <FormControl>
 | 
				
			||||||
 | 
					                      <Input placeholder="johndoe" {...field} />
 | 
				
			||||||
 | 
					                    </FormControl>
 | 
				
			||||||
 | 
					                    <FormMessage />
 | 
				
			||||||
 | 
					                  </FormItem>
 | 
				
			||||||
 | 
					                )}
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <FormField
 | 
				
			||||||
 | 
					                control={form.control}
 | 
				
			||||||
 | 
					                name="email"
 | 
				
			||||||
 | 
					                render={({ field }) => (
 | 
				
			||||||
 | 
					                  <FormItem>
 | 
				
			||||||
 | 
					                    <FormLabel>Email</FormLabel>
 | 
				
			||||||
 | 
					                    <FormControl>
 | 
				
			||||||
 | 
					                      <Input type="email" placeholder="john@example.com" {...field} />
 | 
				
			||||||
 | 
					                    </FormControl>
 | 
				
			||||||
 | 
					                    <FormMessage />
 | 
				
			||||||
 | 
					                  </FormItem>
 | 
				
			||||||
 | 
					                )}
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <FormField
 | 
				
			||||||
 | 
					                control={form.control}
 | 
				
			||||||
 | 
					                name="password"
 | 
				
			||||||
 | 
					                render={({ field }) => (
 | 
				
			||||||
 | 
					                  <FormItem>
 | 
				
			||||||
 | 
					                    <FormLabel>Password</FormLabel>
 | 
				
			||||||
 | 
					                    <FormControl>
 | 
				
			||||||
 | 
					                      <Input type="password" placeholder="********" {...field} />
 | 
				
			||||||
 | 
					                    </FormControl>
 | 
				
			||||||
 | 
					                    <FormMessage />
 | 
				
			||||||
 | 
					                  </FormItem>
 | 
				
			||||||
 | 
					                )}
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <FormField
 | 
				
			||||||
 | 
					                control={form.control}
 | 
				
			||||||
 | 
					                name="confirmPassword"
 | 
				
			||||||
 | 
					                render={({ field }) => (
 | 
				
			||||||
 | 
					                  <FormItem>
 | 
				
			||||||
 | 
					                    <FormLabel>Confirm Password</FormLabel>
 | 
				
			||||||
 | 
					                    <FormControl>
 | 
				
			||||||
 | 
					                      <Input type="password" placeholder="********" {...field} />
 | 
				
			||||||
 | 
					                    </FormControl>
 | 
				
			||||||
 | 
					                    <FormMessage />
 | 
				
			||||||
 | 
					                  </FormItem>
 | 
				
			||||||
 | 
					                )}
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <Button
 | 
				
			||||||
 | 
					                type="submit"
 | 
				
			||||||
 | 
					                className="w-full"
 | 
				
			||||||
 | 
					                disabled={isLoading}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                {isLoading ? "Creating account..." : "Register"}
 | 
				
			||||||
 | 
					              </Button>
 | 
				
			||||||
 | 
					            </form>
 | 
				
			||||||
 | 
					          </Form>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <div className="mt-4 text-center">
 | 
				
			||||||
 | 
					            <p className="text-sm text-muted-foreground">
 | 
				
			||||||
 | 
					              Already have an account?{" "}
 | 
				
			||||||
 | 
					              <a href="/login" className="text-primary hover:underline">
 | 
				
			||||||
 | 
					                Login
 | 
				
			||||||
 | 
					              </a>
 | 
				
			||||||
 | 
					            </p>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </CardContent>
 | 
				
			||||||
 | 
					      </Card>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										46
									
								
								src/pages/VerifyEmail.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/pages/VerifyEmail.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,46 @@
 | 
				
			|||||||
 | 
					import { useEffect, useState } from "react";
 | 
				
			||||||
 | 
					import { useSearchParams, useNavigate } from "react-router-dom";
 | 
				
			||||||
 | 
					import { toast } from "sonner";
 | 
				
			||||||
 | 
					import { verifyEmail } from "@/lib/api";
 | 
				
			||||||
 | 
					import { Button } from "@/components/ui/button";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function VerifyEmail() {
 | 
				
			||||||
 | 
					  const [searchParams] = useSearchParams();
 | 
				
			||||||
 | 
					  const navigate = useNavigate();
 | 
				
			||||||
 | 
					  const [isVerifying, setIsVerifying] = useState(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    const token = searchParams.get("token");
 | 
				
			||||||
 | 
					    if (token) {
 | 
				
			||||||
 | 
					      (async () => {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					          await verifyEmail(token);
 | 
				
			||||||
 | 
					          toast.success("Email verified successfully! Please log in.");
 | 
				
			||||||
 | 
					          // Redirect to login after a short delay
 | 
				
			||||||
 | 
					          setTimeout(() => {
 | 
				
			||||||
 | 
					            navigate("/login");
 | 
				
			||||||
 | 
					          }, 3000);
 | 
				
			||||||
 | 
					        } catch (error: any) {
 | 
				
			||||||
 | 
					          toast.error(error.message || "Email verification failed");
 | 
				
			||||||
 | 
					        } finally {
 | 
				
			||||||
 | 
					          setIsVerifying(false);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      })();
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      toast.error("Verification token not found");
 | 
				
			||||||
 | 
					      setIsVerifying(false);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [navigate, searchParams]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className="flex flex-col items-center justify-center py-10">
 | 
				
			||||||
 | 
					      {isVerifying ? (
 | 
				
			||||||
 | 
					        <p>Verifying your email...</p>
 | 
				
			||||||
 | 
					      ) : (
 | 
				
			||||||
 | 
					        <Button onClick={() => navigate("/login")}>
 | 
				
			||||||
 | 
					          Go to Login
 | 
				
			||||||
 | 
					        </Button>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										1
									
								
								src/vite-env.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/vite-env.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					/// <reference types="vite/client" />
 | 
				
			||||||
							
								
								
									
										76
									
								
								tailwind.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								tailwind.config.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,76 @@
 | 
				
			|||||||
 | 
					const { fontFamily } = require("tailwindcss/defaultTheme");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/** @type {import('tailwindcss').Config} */
 | 
				
			||||||
 | 
					module.exports = {
 | 
				
			||||||
 | 
					  darkMode: ["class"],
 | 
				
			||||||
 | 
					  content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
 | 
				
			||||||
 | 
					  theme: {
 | 
				
			||||||
 | 
					    container: {
 | 
				
			||||||
 | 
					      center: true,
 | 
				
			||||||
 | 
					      padding: "2rem",
 | 
				
			||||||
 | 
					      screens: {
 | 
				
			||||||
 | 
					        "2xl": "1400px",
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    extend: {
 | 
				
			||||||
 | 
					      colors: {
 | 
				
			||||||
 | 
					        border: "hsl(var(--border))",
 | 
				
			||||||
 | 
					        input: "hsl(var(--input))",
 | 
				
			||||||
 | 
					        ring: "hsl(var(--ring))",
 | 
				
			||||||
 | 
					        background: "hsl(var(--background))",
 | 
				
			||||||
 | 
					        foreground: "hsl(var(--foreground))",
 | 
				
			||||||
 | 
					        primary: {
 | 
				
			||||||
 | 
					          DEFAULT: "hsl(221.2 83.2% 53.3%)",
 | 
				
			||||||
 | 
					          foreground: "hsl(var(--primary-foreground))",
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        secondary: {
 | 
				
			||||||
 | 
					          DEFAULT: "hsl(var(--secondary))",
 | 
				
			||||||
 | 
					          foreground: "hsl(var(--secondary-foreground))",
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        destructive: {
 | 
				
			||||||
 | 
					          DEFAULT: "hsl(var(--destructive))",
 | 
				
			||||||
 | 
					          foreground: "hsl(var(--destructive-foreground))",
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        muted: {
 | 
				
			||||||
 | 
					          DEFAULT: "hsl(var(--muted))",
 | 
				
			||||||
 | 
					          foreground: "hsl(var(--muted-foreground))",
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        accent: {
 | 
				
			||||||
 | 
					          DEFAULT: "hsl(var(--accent))",
 | 
				
			||||||
 | 
					          foreground: "hsl(var(--accent-foreground))",
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        popover: {
 | 
				
			||||||
 | 
					          DEFAULT: "hsl(var(--popover))",
 | 
				
			||||||
 | 
					          foreground: "hsl(var(--popover-foreground))",
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        card: {
 | 
				
			||||||
 | 
					          DEFAULT: "hsl(var(--card))",
 | 
				
			||||||
 | 
					          foreground: "hsl(var(--card-foreground))",
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      borderRadius: {
 | 
				
			||||||
 | 
					        lg: "var(--radius)",
 | 
				
			||||||
 | 
					        md: "calc(var(--radius) - 2px)",
 | 
				
			||||||
 | 
					        sm: "calc(var(--radius) - 4px)",
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      fontFamily: {
 | 
				
			||||||
 | 
					        sans: ["var(--font-sans)", ...fontFamily.sans],
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      keyframes: {
 | 
				
			||||||
 | 
					        "accordion-down": {
 | 
				
			||||||
 | 
					          from: { height: 0 },
 | 
				
			||||||
 | 
					          to: { height: "var(--radix-accordion-content-height)" },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "accordion-up": {
 | 
				
			||||||
 | 
					          from: { height: "var(--radix-accordion-content-height)" },
 | 
				
			||||||
 | 
					          to: { height: 0 },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      animation: {
 | 
				
			||||||
 | 
					        "accordion-down": "accordion-down 0.2s ease-out",
 | 
				
			||||||
 | 
					        "accordion-up": "accordion-up 0.2s ease-out",
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  plugins: [require("tailwindcss-animate")],
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										26
									
								
								tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								tsconfig.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "compilerOptions": {
 | 
				
			||||||
 | 
					    "target": "ES2020",
 | 
				
			||||||
 | 
					    "useDefineForClassFields": true,
 | 
				
			||||||
 | 
					    "lib": ["ES2020", "DOM", "DOM.Iterable"],
 | 
				
			||||||
 | 
					    "module": "ESNext",
 | 
				
			||||||
 | 
					    "skipLibCheck": true,
 | 
				
			||||||
 | 
					    "baseUrl": ".",
 | 
				
			||||||
 | 
					    "paths": {
 | 
				
			||||||
 | 
					      "@/*": ["./src/*"]
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /* Bundler mode */
 | 
				
			||||||
 | 
					    "moduleResolution": "bundler",
 | 
				
			||||||
 | 
					    "allowImportingTsExtensions": true,
 | 
				
			||||||
 | 
					    "resolveJsonModule": true,
 | 
				
			||||||
 | 
					    "isolatedModules": true,
 | 
				
			||||||
 | 
					    "noEmit": true,
 | 
				
			||||||
 | 
					    "jsx": "react-jsx",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /* Linting */
 | 
				
			||||||
 | 
					    "strict": true,
 | 
				
			||||||
 | 
					    "noFallthroughCasesInSwitch": true
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "include": ["src", "vite.config.ts"]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										1
									
								
								tsconfig.tsbuildinfo
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								tsconfig.tsbuildinfo
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/ApiKeyItem.tsx","./src/components/FeedActions.tsx","./src/components/FeedChart.tsx","./src/components/Header.tsx","./src/components/Layout.tsx","./src/components/ProtectedRoute.tsx","./src/components/ThemeToggle.tsx","./src/components/ui/alert.tsx","./src/components/ui/avatar.tsx","./src/components/ui/button.tsx","./src/components/ui/calendar.tsx","./src/components/ui/card.tsx","./src/components/ui/date-time-picker.tsx","./src/components/ui/dialog.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/form.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/popover.tsx","./src/components/ui/select.tsx","./src/components/ui/separator.tsx","./src/components/ui/sheet.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/tabs.tsx","./src/components/ui/toast.tsx","./src/components/ui/toaster.tsx","./src/hooks/use-toast.ts","./src/lib/api.ts","./src/lib/auth.ts","./src/lib/store.ts","./src/lib/theme-provider.tsx","./src/lib/timeRanges.ts","./src/lib/types.ts","./src/lib/utils.ts","./src/pages/ApiKeysPage.tsx","./src/pages/DashboardPage.tsx","./src/pages/HomePage.tsx","./src/pages/LoginPage.tsx","./src/pages/ProfilePage.tsx","./src/pages/RegisterPage.tsx","./src/pages/VerifyEmail.tsx","./vite.config.ts"],"version":"5.6.3"}
 | 
				
			||||||
							
								
								
									
										26
									
								
								vite.config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								vite.config.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
				
			|||||||
 | 
					import path from "path";
 | 
				
			||||||
 | 
					import react from "@vitejs/plugin-react";
 | 
				
			||||||
 | 
					import { defineConfig } from "vite";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// https://vitejs.dev/config/
 | 
				
			||||||
 | 
					export default defineConfig({
 | 
				
			||||||
 | 
					  plugins: [react()],
 | 
				
			||||||
 | 
					  resolve: {
 | 
				
			||||||
 | 
					    alias: {
 | 
				
			||||||
 | 
					      "@": path.resolve(__dirname, "./src"),
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  build: {
 | 
				
			||||||
 | 
					    rollupOptions: {
 | 
				
			||||||
 | 
					      output: {
 | 
				
			||||||
 | 
					        manualChunks(id) {
 | 
				
			||||||
 | 
					          if (id.includes('node_modules')) {
 | 
				
			||||||
 | 
					            // Create separate chunk for vendor libraries
 | 
				
			||||||
 | 
					            return 'vendor';
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    chunkSizeWarningLimit: 600, // Adjust if needed
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
		Reference in New Issue
	
	Block a user