diff --git a/.cursorignore b/.cursorignore new file mode 100644 index 0000000..bb1f9e9 --- /dev/null +++ b/.cursorignore @@ -0,0 +1,13 @@ +# Build artifacts +dist/ +build/ +coverage/ + +#3rd party +node_modules/ + +# Deprecated code + +# Experiments and temporary files + +# Old migrations and scripts diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..4349175 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v20.18.3 \ No newline at end of file diff --git a/CLAUDE.md_ b/CLAUDE.md_ new file mode 100644 index 0000000..546ab49 --- /dev/null +++ b/CLAUDE.md_ @@ -0,0 +1,80 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Development Commands + +**Development:** +- `npm run dev` - Start development server with Vite +- `npm run build` - TypeScript build check + production build +- `npm run lint` - Run ESLint +- `npm run preview` - Preview production build locally +- `npm run build:gh` - Build for GitHub Pages deployment (with base URL and hash routing) + +**Type Checking:** +- Use `npm run build` for TypeScript compilation check before production + +## Architecture Overview + +This is a React + TypeScript SPA using Vite, shadcn/ui, and React Router. The application follows a modular structure with centralized configuration and mock API data. + +### Key Architecture Patterns + +**Routing:** +- React Router v7 with nested routes defined in `src/Router.tsx` +- Layout component wraps all routes with consistent header/navigation +- Root path redirects to `/data-centers` + +**State Management:** +- React Query (@tanstack/react-query) for server state +- React Context for theme management (`src/contexts/ThemeContext.tsx`) +- Local component state for UI interactions + +**API Layer:** +- Mock API in `src/lib/api.ts` with simulated async delays +- Centralized types in `src/lib/types.ts` +- Generic `ApiResponse` wrapper for all API responses + +**Configuration:** +- App config in `src/config/app.ts` with environment variable support +- Menu configuration in `src/config/menu.ts` for navigation structure +- Vite config supports base URL override via `VITE_BASE_URL` + +### Component Structure + +**Layout System:** +- `src/components/layout/Layout.tsx` - Main layout wrapper +- `src/components/layout/Header.tsx` - App header with navigation +- Responsive design with container-based layouts + +**UI Components:** +- shadcn/ui components in `src/components/ui/` +- Custom components follow shadcn patterns +- Tailwind CSS v4 with CSS variables for theming + +**Pages:** +- Feature pages in `src/pages/` (DataCenters, Devices) +- Each page handles its own data fetching with React Query + +### Data Models + +**Primary Types:** +- `DataCenter`: Location-based infrastructure with type (On-Premise/Cloud), IP ranges, descriptions +- `Device`: Mobile devices with OS versions, connection status, assigned data centers +- `ApiResponse`: Standardized API response wrapper + +### Development Patterns + +**Styling:** +- Tailwind CSS with shadcn/ui design system +- CSS variables for theme consistency +- Responsive-first approach + +**Import Aliases:** +- `@/` maps to `src/` +- Component categories: `@/components`, `@/lib`, `@/hooks` + +**Environment Variables:** +- `VITE_APP_NAME` - Application name override +- `VITE_BASE_URL` - Base URL for deployment (GitHub Pages support) +- `VITE_USE_HASH_ROUTE` - Enable hash routing for static deployments \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index cd1289f..d5e9968 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,9 +14,12 @@ "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-popover": "^1.1.6", "@radix-ui/react-separator": "^1.1.2", - "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.1.8", "@tailwindcss/vite": "^4.0.14", + "@tanstack/react-query": "^5.80.6", + "@tanstack/react-table": "^8.21.3", + "axios": "^1.9.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.482.0", @@ -28,6 +31,7 @@ "tailwindcss-animate": "^1.0.7" }, "devDependencies": { + "@anthropic-ai/claude-code": "^1.0.51", "@eslint/js": "^9.21.0", "@types/node": "^22.13.10", "@types/react": "^19.0.10", @@ -57,6 +61,27 @@ "node": ">=6.0.0" } }, + "node_modules/@anthropic-ai/claude-code": { + "version": "1.0.51", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code/-/claude-code-1.0.51.tgz", + "integrity": "sha512-annBc4ez7nDPbp+di6bjIQGiAdmdVln4k3gAYioym2MxOEl3py5s7SsoR+dNSmfgfIHUdKBTbtXnXD5rkmO5aA==", + "dev": true, + "license": "SEE LICENSE IN README.md", + "bin": { + "claude": "cli.js" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "^0.33.5", + "@img/sharp-darwin-x64": "^0.33.5", + "@img/sharp-linux-arm": "^0.33.5", + "@img/sharp-linux-arm64": "^0.33.5", + "@img/sharp-linux-x64": "^0.33.5", + "@img/sharp-win32-x64": "^0.33.5" + } + }, "node_modules/@babel/code-frame": { "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", @@ -994,6 +1019,226 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.8", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", @@ -1196,6 +1441,24 @@ } } }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", + "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-compose-refs": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", @@ -1262,6 +1525,24 @@ } } }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", + "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-direction": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", @@ -1431,6 +1712,24 @@ } } }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", + "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-popover": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.6.tgz", @@ -1468,6 +1767,24 @@ } } }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", + "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-popper": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.2.tgz", @@ -1571,6 +1888,24 @@ } } }, + "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", + "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-roving-focus": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.2.tgz", @@ -1626,12 +1961,12 @@ } }, "node_modules/@radix-ui/react-slot": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", - "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.1" + "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", @@ -1643,6 +1978,21 @@ } } }, + "node_modules/@radix-ui/react-slot/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-tooltip": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.8.tgz", @@ -1677,6 +2027,24 @@ } } }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", + "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", @@ -2279,6 +2647,65 @@ "vite": "^5.2.0 || ^6" } }, + "node_modules/@tanstack/query-core": { + "version": "5.80.6", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.80.6.tgz", + "integrity": "sha512-nl7YxT/TAU+VTf+e2zTkObGTyY8YZBMnbgeA1ee66lIVqzKlYursAII6z5t0e6rXgwUMJSV4dshBTNacNpZHbQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.80.6", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.80.6.tgz", + "integrity": "sha512-izX+5CnkpON3NQGcEm3/d7LfFQNo9ZpFtX2QsINgCYK9LT2VCIdi8D3bMaMSNhrAJCznRoAkFic76uvLroALBw==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.80.6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-table": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz", + "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==", + "license": "MIT", + "dependencies": { + "@tanstack/table-core": "8.21.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", + "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2687,6 +3114,23 @@ "node": ">=10" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", + "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2751,6 +3195,19 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2840,6 +3297,18 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2929,6 +3398,15 @@ "dev": true, "license": "MIT" }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/detect-libc": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", @@ -2944,6 +3422,20 @@ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", "license": "MIT" }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.120", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.120.tgz", @@ -2964,6 +3456,51 @@ "node": ">=10.13.0" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz", @@ -3330,6 +3867,42 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", + "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -3344,6 +3917,15 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -3354,6 +3936,30 @@ "node": ">=6.9.0" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-nonce": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", @@ -3363,6 +3969,19 @@ "node": ">=6" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -3389,6 +4008,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -3412,6 +4043,45 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3859,6 +4529,15 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -3883,6 +4562,27 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -4075,6 +4775,12 @@ "node": ">= 0.8.0" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/package.json b/package.json index 10406a4..a74b2ed 100644 --- a/package.json +++ b/package.json @@ -17,9 +17,12 @@ "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-popover": "^1.1.6", "@radix-ui/react-separator": "^1.1.2", - "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.1.8", "@tailwindcss/vite": "^4.0.14", + "@tanstack/react-query": "^5.80.6", + "@tanstack/react-table": "^8.21.3", + "axios": "^1.9.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.482.0", @@ -31,6 +34,7 @@ "tailwindcss-animate": "^1.0.7" }, "devDependencies": { + "@anthropic-ai/claude-code": "^1.0.51", "@eslint/js": "^9.21.0", "@types/node": "^22.13.10", "@types/react": "^19.0.10", diff --git a/src/App.tsx b/src/App.tsx deleted file mode 100644 index 8d4a7db..0000000 --- a/src/App.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { BrowserRouter, HashRouter } from 'react-router' -import { ThemeProvider } from './contexts/ThemeContext' -import Router from './Router' - -const AppRouter = import.meta.env.VITE_USE_HASH_ROUTE === 'true' ? HashRouter : BrowserRouter - -export default function App() { - return ( - - - - - - ) -} diff --git a/src/Router.tsx b/src/Router.tsx index 478cf2f..78e424f 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -1,21 +1,25 @@ -import { Routes, Route } from 'react-router-dom' -import { AppLayout } from './components/app-layout' -import NotMatch from './pages/NotMatch' -import Dashboard from './pages/Dashboard' -import Sample from './pages/Sample' -import ComingSoon from './pages/ComingSoon' +import { createBrowserRouter, Navigate } from "react-router-dom"; +import { Layout } from "./components/layout/Layout"; +import DataCenters from "./pages/DataCenters"; +import Devices from "./pages/Devices"; -export default function Router() { - return ( - - }> - } /> - - } /> - } /> - - } /> - - - ) -} +export const router = createBrowserRouter([ + { + path: "/", + element: , + children: [ + { + index: true, + element: , + }, + { + path: "data-centers", + element: , + }, + { + path: "devices", + element: , + }, + ], + }, +]); diff --git a/src/components/app-footer.tsx b/src/components/app-footer.tsx index 6bd619a..5d57c98 100644 --- a/src/components/app-footer.tsx +++ b/src/components/app-footer.tsx @@ -1,15 +1,11 @@ -import { appConfig } from '@/config/app' -import { ModeToggle } from './mode-toggle' +import { ModeToggle } from "./mode-toggle"; export function AppFooter() { - return ( - - ) -} \ No newline at end of file + return ( +
+
+ +
+
+ ); +} diff --git a/src/components/app-header.tsx b/src/components/app-header.tsx index ab2a1f4..da65132 100644 --- a/src/components/app-header.tsx +++ b/src/components/app-header.tsx @@ -1,129 +1,128 @@ -import { Link, NavLink, useLocation } from 'react-router-dom' -import { mainMenu } from '@/config/menu' -import { cn } from '@/lib/utils' +import { Link, NavLink, useLocation } from "react-router-dom"; +import { mainMenu } from "@/config/menu"; +import { cn } from "@/lib/utils"; import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu" -import { ChevronDown } from 'lucide-react' -import { AppLogo } from './app-logo' -import { AppSidebar } from './app-sidebar' -import { Button, buttonVariants } from './ui/button' -import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar' -import { appConfig, baseUrl } from '@/config/app' -import GitHub from './icons/github' + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { ChevronDown } from "lucide-react"; +import { AppLogo } from "./app-logo"; +import { AppSidebar } from "./app-sidebar"; +import { Button } from "./ui/button"; +import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar"; +import { baseUrl } from "@/config/app"; export function AppHeader() { - const location = useLocation() + const location = useLocation(); - return ( -
-
-
- - - - -
+ return ( +
+
+
+ + + + +
-
-
- -
-
-
- ) -} \ No newline at end of file + "cursor-pointer", + subItem.url === location.pathname && "bg-muted" + )} + > + {subItem.title} + + + ))} + + + ) : ( + + cn( + "flex items-center gap-2 overflow-hidden rounded-md p-2.5 text-left text-sm outline-none transition-[width,height,padding] hover:bg-accent hover:text-accent-foreground focus-visible:ring-2 active:bg-accent active:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>svg]:size-4", + "h-8 text-sm hover:bg-accent hover:text-accent-foreground", + isActive + ? "text-foreground bg-accent" + : "text-foreground/70" + ) + } + > + {item.icon && } + {item.title} + + ) + )} + +
+ + + +
+ ); +} diff --git a/src/components/app-layout.tsx b/src/components/app-layout.tsx deleted file mode 100644 index 74d7fa3..0000000 --- a/src/components/app-layout.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Outlet } from 'react-router' -import { AppHeader } from './app-header' -import { AppFooter } from './app-footer' - -export function AppLayout() { - return ( -
- -
-
- -
- -
-
- ) -} \ No newline at end of file diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx new file mode 100644 index 0000000..08df231 --- /dev/null +++ b/src/components/layout/Header.tsx @@ -0,0 +1,44 @@ +import { Link, useLocation } from "react-router-dom"; +import { cn } from "@/lib/utils"; +import { Database, Server } from "lucide-react"; + +export function Header() { + const location = useLocation(); + + const isActive = (path: string) => location.pathname === path; + + return ( +
+
+
+ + DCMS +
+ +
+
+ ); +} diff --git a/src/components/layout/Layout.tsx b/src/components/layout/Layout.tsx new file mode 100644 index 0000000..c526fb6 --- /dev/null +++ b/src/components/layout/Layout.tsx @@ -0,0 +1,13 @@ +import { Outlet } from "react-router-dom"; +import { Header } from "./Header"; + +export function Layout() { + return ( +
+
+
+ +
+
+ ); +} diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx new file mode 100644 index 0000000..0205413 --- /dev/null +++ b/src/components/ui/badge.tsx @@ -0,0 +1,46 @@ +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 badgeVariants = cva( + "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Badge({ + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<"span"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : "span" + + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/src/components/ui/search-input.tsx b/src/components/ui/search-input.tsx new file mode 100644 index 0000000..ef76c8f --- /dev/null +++ b/src/components/ui/search-input.tsx @@ -0,0 +1,19 @@ +import * as React from "react"; +import { Search as SearchIcon } from "lucide-react"; + +interface SearchInputProps extends React.InputHTMLAttributes { + className?: string; +} + +export function SearchInput({ className, ...props }: SearchInputProps) { + return ( +
+ + +
+ ); +} diff --git a/src/components/ui/table.tsx b/src/components/ui/table.tsx new file mode 100644 index 0000000..5513a5c --- /dev/null +++ b/src/components/ui/table.tsx @@ -0,0 +1,114 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Table({ className, ...props }: React.ComponentProps<"table">) { + return ( +
+ + + ) +} + +function TableHeader({ className, ...props }: React.ComponentProps<"thead">) { + return ( + + ) +} + +function TableBody({ className, ...props }: React.ComponentProps<"tbody">) { + return ( + + ) +} + +function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) { + return ( + tr]:last:border-b-0", + className + )} + {...props} + /> + ) +} + +function TableRow({ className, ...props }: React.ComponentProps<"tr">) { + return ( + + ) +} + +function TableHead({ className, ...props }: React.ComponentProps<"th">) { + return ( +
[role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> + ) +} + +function TableCell({ className, ...props }: React.ComponentProps<"td">) { + return ( + [role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> + ) +} + +function TableCaption({ + className, + ...props +}: React.ComponentProps<"caption">) { + return ( +
+ ) +} + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +} diff --git a/src/config/menu.ts b/src/config/menu.ts index d5ed6de..4e1860f 100644 --- a/src/config/menu.ts +++ b/src/config/menu.ts @@ -1,43 +1,23 @@ -import { - CircleAlert, - Files, - Gauge, - LucideIcon -} from 'lucide-react' +import { Files, Gauge, LucideIcon } from "lucide-react"; type MenuItemType = { - title: string - url: string - external?: string - icon?: LucideIcon - items?: MenuItemType[] -} -type MenuType = MenuItemType[] + title: string; + url: string; + external?: string; + icon?: LucideIcon; + items?: MenuItemType[]; +}; +type MenuType = MenuItemType[]; export const mainMenu: MenuType = [ - { - title: 'Dashboard', - url: '/', - icon: Gauge - }, - { - title: 'Pages', - url: '/pages', - icon: Files, - items: [ - { - title: 'Sample Page', - url: '/pages/sample', - }, - { - title: 'Coming Soon', - url: '/pages/feature', - }, - ] - }, - { - title: 'Error', - url: '/404', - icon: CircleAlert, - }, -] + { + title: "Dashboard", + url: "/", + icon: Gauge, + }, + { + title: "Sample Page", + url: "/sample", + icon: Files, + }, +]; diff --git a/src/lib/api.ts b/src/lib/api.ts new file mode 100644 index 0000000..d58d938 --- /dev/null +++ b/src/lib/api.ts @@ -0,0 +1,143 @@ +import { DataCenter, Device, ApiResponse } from "./types"; + +// Mock data +const mockDataCenters: DataCenter[] = [ + { + id: "dc1", + location: "New York", + type: "On-Premise", + ipRange: "192.168.1.0/24", + description: "Main data center", + }, + { + id: "dc2", + location: "Los Angeles", + type: "Cloud", + ipRange: "10.0.0.0/16", + description: "Cloud data center", + }, + { + id: "dc3", + location: "Chicago", + type: "On-Premise", + ipRange: "172.16.0.0/20", + description: "Secondary data center", + }, + { + id: "dc4", + location: "London", + type: "Cloud", + ipRange: "10.1.0.0/16", + description: "International cloud data center", + }, + { + id: "dc5", + location: "Tokyo", + type: "On-Premise", + ipRange: "192.168.2.0/24", + description: "Asia data center", + }, +]; + +const mockDevices: Device[] = [ + { + id: "Device-001", + model: "Galaxy S21", + osVersion: "Android 12", + status: "connected", + dataCenter: "DC-West", + }, + { + id: "Device-002", + model: "iPhone 13 Pro", + osVersion: "iOS 15.4", + status: "connected", + dataCenter: "DC-East", + }, + { + id: "Device-003", + model: "Pixel 6", + osVersion: "Android 13", + status: "disconnected", + dataCenter: "DC-Central", + }, + { + id: "Device-004", + model: "Galaxy S20", + osVersion: "Android 11", + status: "connected", + dataCenter: "DC-West", + }, + { + id: "Device-005", + model: "iPhone 12", + osVersion: "iOS 14.8", + status: "disconnected", + dataCenter: "DC-East", + }, + { + id: "Device-006", + model: "Pixel 5a", + osVersion: "Android 12", + status: "connected", + dataCenter: "DC-Central", + }, + { + id: "Device-007", + model: "Galaxy S10", + osVersion: "Android 10", + status: "disconnected", + dataCenter: "DC-West", + }, + { + id: "Device-008", + model: "iPhone 11", + osVersion: "iOS 13.7", + status: "connected", + dataCenter: "DC-East", + }, + { + id: "Device-009", + model: "Pixel 4a", + osVersion: "Android 11", + status: "disconnected", + dataCenter: "DC-Central", + }, + { + id: "Device-010", + model: "Galaxy S9", + osVersion: "Android 9", + status: "connected", + dataCenter: "DC-West", + }, +]; + +// API client +const api = { + getDataCenters: async (): Promise> => { + // Simulate API delay + await new Promise((resolve) => setTimeout(resolve, 1000)); + return { data: mockDataCenters }; + }, + + getDevices: async (dataCenterId?: string): Promise> => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + const devices = dataCenterId + ? mockDevices.filter((device) => device.dataCenter === dataCenterId) + : mockDevices; + return { data: devices }; + }, + + getDataCenter: async ( + id: string + ): Promise> => { + await new Promise((resolve) => setTimeout(resolve, 500)); + const dataCenter = mockDataCenters.find((dc) => dc.id === id); + if (!dataCenter) { + return { data: null, error: "Data center not found" }; + } + return { data: dataCenter }; + }, +}; + +export default api; diff --git a/src/lib/types.ts b/src/lib/types.ts new file mode 100644 index 0000000..0dd0db9 --- /dev/null +++ b/src/lib/types.ts @@ -0,0 +1,20 @@ +export type DataCenter = { + id: string; + location: string; + type: "On-Premise" | "Cloud"; + ipRange: string; + description: string; +}; + +export type Device = { + id: string; + model: string; + osVersion: string; + status: "connected" | "disconnected"; + dataCenter: string; +}; + +export type ApiResponse = { + data: T; + error?: string; +}; diff --git a/src/main.tsx b/src/main.tsx index bef5202..6d8d698 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,10 +1,16 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import './index.css' -import App from './App.tsx' +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { RouterProvider } from "react-router-dom"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { router } from "./Router"; +import "./index.css"; -createRoot(document.getElementById('root')!).render( +const queryClient = new QueryClient(); + +createRoot(document.getElementById("root")!).render( - - , -) + + + + +); diff --git a/src/pages/ComingSoon.tsx b/src/pages/ComingSoon.tsx deleted file mode 100644 index 5661f1c..0000000 --- a/src/pages/ComingSoon.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { PageHeader, PageHeaderHeading } from '@/components/page-header' -import { Card, CardContent } from '@/components/ui/card' - -export default function ComingSoon() { - return ( - <> - - Coming Soon - - - -

Coming soon feature ...

-
-
- - ) -} \ No newline at end of file diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx deleted file mode 100644 index 7c8e528..0000000 --- a/src/pages/Dashboard.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { PageHeader, PageHeaderHeading } from "@/components/page-header"; -import { Card, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; - -export default function Dashboard() { - return ( - <> - - Dashboard - - - - React Shadcn Starter - React + Vite + TypeScript template for building apps with shadcn/ui. - - - - ) -} diff --git a/src/pages/DataCenters.tsx b/src/pages/DataCenters.tsx new file mode 100644 index 0000000..ccd976c --- /dev/null +++ b/src/pages/DataCenters.tsx @@ -0,0 +1,155 @@ +import { useQuery } from "@tanstack/react-query"; +import { + ColumnDef, + flexRender, + getCoreRowModel, + useReactTable, +} from "@tanstack/react-table"; +import { DataCenter } from "@/lib/types"; +import api from "@/lib/api"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { Loader2 } from "lucide-react"; + +const columns: ColumnDef[] = [ + { + accessorKey: "location", + header: "Location", + cell: ({ row }) => { + const location = row.getValue("location") as string; + return ( + + {location} + + ); + }, + }, + { + accessorKey: "type", + header: "Type", + cell: ({ row }) => { + const type = row.getValue("type") as string; + return ( + + {type} + + ); + }, + }, + { + accessorKey: "ipRange", + header: "IP Range", + cell: ({ row }) => { + const ipRange = row.getValue("ipRange") as string; + return ( + + {ipRange} + + ); + }, + }, + { + accessorKey: "description", + header: "Description", + cell: ({ row }) => { + const description = row.getValue("description") as string; + return ( + + {description} + + ); + }, + }, +]; + +export default function DataCenters() { + const { data: dataCenters, isLoading } = useQuery({ + queryKey: ["dataCenters"], + queryFn: async () => { + const response = await api.getDataCenters(); + return response.data; + }, + }); + + const table = useReactTable({ + data: dataCenters || [], + columns, + getCoreRowModel: getCoreRowModel(), + }); + + if (isLoading) { + return ( +
+ +
+ ); + } + + return ( +
+
+
+

Data Centers

+
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ))} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + No data centers found. + + + )} + +
+
+
+
+ ); +} diff --git a/src/pages/Devices.tsx b/src/pages/Devices.tsx new file mode 100644 index 0000000..dbffaee --- /dev/null +++ b/src/pages/Devices.tsx @@ -0,0 +1,237 @@ +import { useQuery } from "@tanstack/react-query"; +import * as React from "react"; +import { + ColumnDef, + flexRender, + getCoreRowModel, + useReactTable, +} from "@tanstack/react-table"; +import { Device } from "@/lib/types"; +import api from "@/lib/api"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { Loader2, X as XIcon } from "lucide-react"; +import { SearchInput } from "@/components/ui/search-input"; + +const columns: ColumnDef[] = [ + { + accessorKey: "id", + header: "ID", + cell: ({ row }) => { + const id = row.getValue("id") as string; + return ( + + {id} + + ); + }, + }, + { + accessorKey: "model", + header: "Model", + cell: ({ row }) => { + const model = row.getValue("model") as string; + return ( + + {model} + + ); + }, + }, + { + accessorKey: "osVersion", + header: "OS and Version", + cell: ({ row }) => { + const osVersion = row.getValue("osVersion") as string; + return ( + + {osVersion} + + ); + }, + }, + { + accessorKey: "status", + header: "Status", + cell: ({ row }) => { + const status = row.getValue("status") as string; + const isConnected = status === "connected"; + return ( +
+ + {isConnected ? "Connected" : "Disconnected"} + +
+ ); + }, + }, + { + accessorKey: "dataCenter", + header: "Data Center", + cell: ({ row }) => { + const dataCenter = row.getValue("dataCenter") as string; + return ( + + {dataCenter} + + ); + }, + }, + { + id: "actions", + header: () => ( + + Actions + + ), + cell: ({ row }) => { + const status = row.getValue("status") as string; + const isConnected = status === "connected"; + return ( + + ); + }, + }, +]; + +export default function Devices() { + const [search, setSearch] = React.useState(""); + const { data: devices, isLoading } = useQuery({ + queryKey: ["devices"], + queryFn: async () => { + const response = await api.getDevices(); + return response.data; + }, + }); + + const filteredDevices = React.useMemo(() => { + if (!devices) return []; + if (!search.trim()) return devices; + return devices.filter((device) => + [device.id, device.model, device.osVersion, device.dataCenter] + .join(" ") + .toLowerCase() + .includes(search.toLowerCase()) + ); + }, [devices, search]); + + const table = useReactTable({ + data: filteredDevices, + columns, + getCoreRowModel: getCoreRowModel(), + }); + + if (isLoading) { + return ( +
+ +
+ ); + } + + return ( +
+
+
+

+ Devices +

+
+
+
+ setSearch(e.target.value)} + className="w-full bg-white border border-[#E5E8EB] rounded-lg" + /> + {search && ( + + )} +
+
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ))} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row, rowIndex) => ( + + {row.getVisibleCells().map((cell, cellIndex) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + No devices found. + + + )} + +
+
+
+
+ ); +} diff --git a/src/pages/NotMatch.tsx b/src/pages/NotMatch.tsx deleted file mode 100644 index c05aabb..0000000 --- a/src/pages/NotMatch.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { buttonVariants } from "@/components/ui/button"; -import { Link } from "react-router-dom"; - -export default function NotMatch() { - return ( -
-
-

404

-

Oops! Page not found

-

We are sorry, but the page you requested was not found

- Back to Home -
-
- ) -} diff --git a/src/pages/Sample.tsx b/src/pages/Sample.tsx deleted file mode 100644 index b1c09b2..0000000 --- a/src/pages/Sample.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { PageHeader, PageHeaderHeading } from "@/components/page-header"; -import { Card, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; - -export default function Sample() { - return ( - <> - - Sample Page - - - - Card Title - Card description. - - - - ) -} diff --git a/tsconfig.app.json b/tsconfig.app.json index b6f0da4..9c44b40 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -1,6 +1,5 @@ { "compilerOptions": { - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "target": "ES2020", "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], @@ -8,8 +7,7 @@ "skipLibCheck": true, /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, + "moduleResolution": "node", "isolatedModules": true, "moduleDetection": "force", "noEmit": true, @@ -20,13 +18,10 @@ "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true, "baseUrl": ".", "paths": { - "@/*": [ - "./src/*" - ] + "@/*": ["./src/*"] } }, "include": ["src"]