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